GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — gh-154904: Speed up import of shutil by probing compression extensions by maxday · Pull Request #154908 · python/cpython Conversation
|
| before | after | delta | |
|---|---|---|---|
import shutil |
12.75 ms | 10.25 ms | −2.50 ms (−19.6%) |
import urllib.request |
36.64 ms | 34.30 ms | −2.34 ms (−6.4%) |
urllib.request benefits via tempfile→shutil. Motivation is short-lived processes on constrained hardware (CLI tools, serverless cold starts).
No API change: flags, both registries and get_{archive,unpack}_formats() are unchanged, including on builds missing a compression library. The probe is a real import, so ImportError semantics are exact, unlike find_spec('bz2'), which resolves the always-present wrapper and reports success where _bz2 failed to compile (the case on my machine). PEP 810 lazy import also won't do: the flags are read at module scope (shutil.py:1145, :1354), so must be final at import time.
Added to TestArchives: test_compression_supported_flags pins the flags to real import semantics, test_compression_wrappers_not_imported_by_shutil prevents the eager imports returning. Existing test_unpack_archive_* coverage unchanged; patchcheck clean.
$ ./python -m test test_shutil -v -m "test_compression*" ok, ok
$ ./python -m test test_shutil test_zipfile test_tarfile test_importlib \
test_site test_venv test_zipimport test_bz2 test_lzma test_zstd \
test_gzip test_urllib2 test_logging -j6
SUCCESS — 14 OK, run=3,959 skipped=314 (test_bz2 skips on main too)
negative controls, each test fails without the half it covers:
find_spec variant: ..._supported_flags FAIL: True != False
unpatched main: ..._wrappers_not_imported FAIL:
"['lzma', 'compression', 'compression.zstd']" != '[]'
fixes: #154904
Sorry, something went wrong.
There was an error while loading. Please reload this page.
…ensions shutil imported bz2, lzma and compression.zstd at module scope only to set the _*_SUPPORTED flags, then discarded the bindings. Executing those pure Python wrappers also pulled in compression._common[._streams] and compression.zstd._zstdfile, a cost paid by every program that imports shutil even if it never touches an archive. Their only importable dependency that may be missing is the extension module each one wraps, so probe _bz2, _lzma and _zstd directly instead. This keeps the exact ImportError semantics of the previous code -- unlike a find_spec() check, which resolves the always-present wrapper and would report success on builds where the extension fails to load. The wrappers are still imported by tarfile/zipfile when an archive is actually created or extracted. Cuts the net cost of "import shutil" by ~2.5 ms (-19.6%) and of "import urllib.request" by ~2.3 ms (-6.4%).
There was an error while loading. Please reload this page.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| def test_compression_wrappers_not_imported_by_shutil(self): | ||
| # Importing shutil must not pull in the compression wrappers: they are | ||
| # only needed once an archive is actually created or extracted, and | ||
| # importing them measurably slows down every process that uses shutil. | ||
| wrappers = ('bz2', 'lzma', 'compression', 'compression.zstd') | ||
| script = ( | ||
| 'import sys, shutil; ' | ||
| f'print([m for m in {wrappers!r} if m in sys.modules])' | ||
| ) | ||
| # -I so that a sitecustomize/usercustomize importing one of these | ||
| # cannot make the test fail spuriously. | ||
| rc, stdout, stderr = assert_python_ok('-I', '-c', script) | ||
| self.assertEqual(stdout.decode().strip(), '[]', stderr) |
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can use ensure_lazy_imports to make it cleaner:
| def test_compression_wrappers_not_imported_by_shutil(self): | |
| # Importing shutil must not pull in the compression wrappers: they are | |
| # only needed once an archive is actually created or extracted, and | |
| # importing them measurably slows down every process that uses shutil. | |
| wrappers = ('bz2', 'lzma', 'compression', 'compression.zstd') | |
| script = ( | |
| 'import sys, shutil; ' | |
| f'print([m for m in {wrappers!r} if m in sys.modules])' | |
| ) | |
| # -I so that a sitecustomize/usercustomize importing one of these | |
| # cannot make the test fail spuriously. | |
| rc, stdout, stderr = assert_python_ok('-I', '-c', script) | |
| self.assertEqual(stdout.decode().strip(), '[]', stderr) | |
| def test_compression_wrappers_not_imported_by_shutil(self): | |
| # gh-154904: Importing shutil must not pull in the compression wrappers: they are | |
| # only needed once an archive is actually created or extracted, and | |
| # importing them measurably slows down every process that uses shutil. | |
| ensure_lazy_imports("shutil", {"bz2", "lzma", "compression", "compression.zstd"}) |
I'm not test it, and you need to add the import statement at the top of this file:
from test.support.import_helper import ensure_lazy_imports
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I'm not quite sure if |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Per review feedback on pythonGH-154908: - Rewrite test_compression_wrappers_not_imported_by_shutil on top of test.support.import_helper.ensure_lazy_imports instead of hand-rolling an assert_python_ok subprocess. - Remove test_compression_supported_flags: since each wrapper imports its extension module at the top level, the assertion is near-tautological, and the zlib subtest compared importing zlib with itself.
There was an error while loading. Please reload this page.
|
Thanks for the review @aisk, both applied. One open question: I left off |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
I guess it's the behavior of don't add a placeholder object to the module, when lazy importing, is the implement detail of CPython, so added the decorator to let other implementation to skip this. In this PR, we don't use lazy import, so we should not add it. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Thanks @aisk leaving it as is then! Let me know if you have other comments |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
cc @emmatyping who may be interested in this. Another idea, replace the three from importlib.util import find_spec _BZ2_SUPPORTED = find_spec('_bz2') is not None _LZMA_SUPPORTED = find_spec('_lzma') is not None _ZSTD_SUPPORTED = find_spec('_zstd') is not None Gives:
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Thanks @hugovk! I did try that first, and it's what pushed me to the find_spec('_bz2') returns a spec as soon as ExtensionFileLoader finds the .so on the path. Everything that happens after that: dlopen, symbol resolution, PyInit__bz2, module exec, is where a broken extension actually fails, and find_spec never gets there. Let's say the extension is compiled against a library that isn't present at runtime (ie: missing/mismatched libbz2). The .so is on disk, dlopen fails: Let me know if I'm missing something, happy to use find_spec if you think that scenario can not happen but it might feels a bit safer to use import? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Ah yes, it is a change in behaviour. With ValueError: unknown archive format 'bztar' With tarfile.CompressionError: bz2 module is not available Which is maybe more informative? Anyway, it's probably not worth changing the behaviour for this small extra speedup, especially as |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Thanks for confirming! |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
The feature freeze has passed, so it will land only in 3.16. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
ok! Thanks for confirming @StanFromIreland :) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
shutil: avoid importing bz2, lzma and compression.zstd just to probe availability
There was an error while loading. Please reload this page.