Skip to content

Commit 1196c34

Browse files
miss-islingtonlpyu001emmatyping
authored
[3.15] gh-155152: Improve performance of concatenated zstd frames in zipimport (GH-155154) (#160565)
gh-155152: Improve performance of concatenated zstd frames in zipimport (GH-155154) (cherry picked from commit 95e088e) Co-authored-by: Pengyu Lee <lipengyu@kylinos.cn> Co-authored-by: Emma Smith <emma@emmatyping.dev>
1 parent e736c96 commit 1196c34

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
@@ -590,12 +590,13 @@ def _get_zlib_decompress_func():
590590

591591
_importing_zstd = False
592592
_zstd_decompressor_class = None
593+
_zstd_get_frame_size = None
593594

594595
# Return the _zstd.ZstdDecompressor function object, or NULL if _zstd couldn't
595596
# be imported. The result is cached when found.
596597
def _get_zstd_decompressor_class():
597-
global _zstd_decompressor_class
598-
if _zstd_decompressor_class:
598+
global _zstd_decompressor_class, _zstd_get_frame_size
599+
if _zstd_decompressor_class and _zstd_get_frame_size:
599600
return _zstd_decompressor_class
600601

601602
global _importing_zstd
@@ -607,7 +608,10 @@ def _get_zstd_decompressor_class():
607608

608609
_importing_zstd = True
609610
try:
610-
from _zstd import ZstdDecompressor as _zstd_decompressor_class
611+
from _zstd import (
612+
ZstdDecompressor as _zstd_decompressor_class,
613+
get_frame_size as _zstd_get_frame_size,
614+
)
611615
except Exception:
612616
_bootstrap._verbose_message("zipimport: zstd UNAVAILABLE")
613617
raise ZipImportError("can't decompress data; zstd not available")
@@ -621,16 +625,24 @@ def _get_zstd_decompressor_class():
621625
def _zstd_decompress(data):
622626
# A simple version of compression.zstd.decompress() as we cannot import
623627
# that here as the stdlib itself could be being zipimported.
628+
decomp_class = _get_zstd_decompressor_class()
629+
data = memoryview(data)
630+
if not data:
631+
raise ZipImportError("zipimport: zstd compressed data ended before "
632+
"the end-of-stream marker")
624633
results = []
625-
while True:
626-
decomp = _get_zstd_decompressor_class()()
627-
results.append(decomp.decompress(data))
634+
while data:
635+
try:
636+
frame_size = _zstd_get_frame_size(data)
637+
except Exception:
638+
raise ZipImportError("zipimport: zstd compressed data ended before "
639+
"the end-of-stream marker") from None
640+
decomp = decomp_class()
641+
results.append(decomp.decompress(data[:frame_size]))
628642
if not decomp.eof:
629643
raise ZipImportError("zipimport: zstd compressed data ended before "
630644
"the end-of-stream marker")
631-
data = decomp.unused_data
632-
if not data:
633-
break
645+
data = data[frame_size:]
634646
return b"".join(results)
635647

636648

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)