Skip to content

Commit 95e088e

Browse files
lpyu001emmatyping
andauthored
gh-155152: Improve performance of concatenated zstd frames in zipimport (#155154)
Co-authored-by: Emma Smith <emma@emmatyping.dev>
1 parent dc28c3a commit 95e088e

3 files changed

Lines changed: 78 additions & 9 deletions

File tree

‎Lib/test/test_zipimport.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,62 @@ class DeflateCompressedZipImportTestCase(UncompressedZipImportTestCase):
11021102
class ZStdCompressedZipImportTestCase(UncompressedZipImportTestCase):
11031103
compression = ZIP_ZSTANDARD
11041104

1105+
def test_concatenated_frames(self):
1106+
from compression import zstd
1107+
import zipfile
1108+
1109+
class ConcatenatedFrameCompressor:
1110+
def compress(self, data):
1111+
return b"".join(zstd.compress(bytes([byte])) for byte in data)
1112+
1113+
def flush(self):
1114+
return b""
1115+
1116+
with unittest.mock.patch.object(
1117+
zipfile,
1118+
"_get_compressor",
1119+
return_value=ConcatenatedFrameCompressor(),
1120+
):
1121+
self.doTest(".py", {TESTMOD + ".py": test_src}, TESTMOD)
1122+
1123+
def test_concatenated_frames_linear_input(self):
1124+
from compression import zstd
1125+
1126+
frame_count = 100
1127+
frame = zstd.compress(b"x")
1128+
compressed = frame * frame_count
1129+
input_sizes = []
1130+
decompressor_class = zipimport._get_zstd_decompressor_class()
1131+
1132+
class RecordingDecompressor:
1133+
def __init__(self):
1134+
self._decompressor = decompressor_class()
1135+
1136+
def __getattr__(self, name):
1137+
return getattr(self._decompressor, name)
1138+
1139+
def decompress(self, data, max_length=-1):
1140+
input_sizes.append(memoryview(data).nbytes)
1141+
return self._decompressor.decompress(data, max_length)
1142+
1143+
with unittest.mock.patch.object(
1144+
zipimport, "_zstd_decompressor_class", RecordingDecompressor
1145+
):
1146+
self.assertEqual(
1147+
zipimport._zstd_decompress(compressed), b"x" * frame_count
1148+
)
1149+
1150+
self.assertEqual(sum(input_sizes), len(compressed))
1151+
1152+
def test_truncated_frame(self):
1153+
from compression import zstd
1154+
1155+
compressed = zstd.compress(b"x")
1156+
for data in (b"", compressed[:-1]):
1157+
with self.subTest(data=data):
1158+
with self.assertRaises(zipimport.ZipImportError):
1159+
zipimport._zstd_decompress(data)
1160+
11051161

11061162
class BadFileZipImportTestCase(unittest.TestCase):
11071163
def assertZipFailure(self, filename):

‎Lib/zipimport.py‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -585,12 +585,13 @@ def _get_zlib_decompress_func():
585585

586586
_importing_zstd = False
587587
_zstd_decompressor_class = None
588+
_zstd_get_frame_size = None
588589

589590
# Return the _zstd.ZstdDecompressor function object, or NULL if _zstd couldn't
590591
# be imported. The result is cached when found.
591592
def _get_zstd_decompressor_class():
592-
global _zstd_decompressor_class
593-
if _zstd_decompressor_class:
593+
global _zstd_decompressor_class, _zstd_get_frame_size
594+
if _zstd_decompressor_class and _zstd_get_frame_size:
594595
return _zstd_decompressor_class
595596

596597
global _importing_zstd
@@ -602,7 +603,10 @@ def _get_zstd_decompressor_class():
602603

603604
_importing_zstd = True
604605
try:
605-
from _zstd import ZstdDecompressor as _zstd_decompressor_class
606+
from _zstd import (
607+
ZstdDecompressor as _zstd_decompressor_class,
608+
get_frame_size as _zstd_get_frame_size,
609+
)
606610
except Exception:
607611
_bootstrap._verbose_message("zipimport: zstd UNAVAILABLE")
608612
raise ZipImportError("can't decompress data; zstd not available")
@@ -616,16 +620,24 @@ def _get_zstd_decompressor_class():
616620
def _zstd_decompress(data):
617621
# A simple version of compression.zstd.decompress() as we cannot import
618622
# that here as the stdlib itself could be being zipimported.
623+
decomp_class = _get_zstd_decompressor_class()
624+
data = memoryview(data)
625+
if not data:
626+
raise ZipImportError("zipimport: zstd compressed data ended before "
627+
"the end-of-stream marker")
619628
results = []
620-
while True:
621-
decomp = _get_zstd_decompressor_class()()
622-
results.append(decomp.decompress(data))
629+
while data:
630+
try:
631+
frame_size = _zstd_get_frame_size(data)
632+
except Exception:
633+
raise ZipImportError("zipimport: zstd compressed data ended before "
634+
"the end-of-stream marker") from None
635+
decomp = decomp_class()
636+
results.append(decomp.decompress(data[:frame_size]))
623637
if not decomp.eof:
624638
raise ZipImportError("zipimport: zstd compressed data ended before "
625639
"the end-of-stream marker")
626-
data = decomp.unused_data
627-
if not data:
628-
break
640+
data = data[frame_size:]
629641
return b"".join(results)
630642

631643

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve performance of some :mod:`zipimport` archives that use Zstandard compression.

0 commit comments

Comments
 (0)