Skip to content

zipimport._zstd_decompress() repeatedly processes remaining input for concatenated frames #155152

Description

@lpyu001

Bug description

zipimport._zstd_decompress() handles concatenated Zstandard frames by creating a new _zstd.ZstdDecompressor for each frame. Each decompressor is passed the entire remaining compressed suffix, and the next iteration obtains the same suffix, minus the frame just decoded, through unused_data.

As a result, when a zstd-compressed ZIP entry using compression method 93 contains many concatenated frames, most of the compressed input is passed to decompressor calls repeatedly. For N similarly sized frames, the cumulative amount of input handled across those calls grows quadratically rather than linearly with the size of the entry.

Reproducer and local results

importtimeimportzipimportfromcompressionimportzstdFRAMES=64_000frame=zstd.compress(b"x")
data=frame*FRAMESinput_sizes= []
original=zipimport._get_zstd_decompressor_class()
classRecordingDecompressor:
def__init__(self, *args, **kwargs):
self._decompressor=original(*args, **kwargs)
def__getattr__(self, name):
returngetattr(self._decompressor, name)
defdecompress(self, data, max_length=-1):
input_sizes.append(memoryview(data).nbytes)
returnself._decompressor.decompress(data, max_length)
zipimport._zstd_decompressor_class=RecordingDecompressorstart=time.perf_counter()
try:
output=zipimport._zstd_decompress(data)
finally:
elapsed=time.perf_counter() -startzipimport._zstd_decompressor_class=originalprint(f"input size: {len(data):,} bytes")
print(f"output size: {len(output):,} bytes")
print(f"decompress calls: {len(input_sizes):,}")
print(f"cumulative input passed: {sum(input_sizes):,} bytes")
print(f"amplification: {sum(input_sizes) /len(data):,.2f}x")
print(f"elapsed: {elapsed:.4f} seconds")

Local result:

input size: 640,000 bytes
output size: 64,000 bytes
decompress calls: 64,000
cumulative input passed: 20,480,320,000 bytes
amplification: 32,000.50x
elapsed: 1.5364 seconds

After fix:

input size: 640,000 bytes
output size: 64,000 bytes
decompress calls: 64,000
cumulative input passed: 640,000 bytes
amplification: 1.00x
elapsed: 0.2333 seconds

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions