Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ Changelog

version 1.10.0-dev
------------------
+ Speed up ``isal_zlib.decompress`` and ``igzip_lib.decompress`` for gzip data
by starting with an output buffer of the size stored in the gzip trailer, up
to 32 MiB, instead of a 16 KiB buffer that doubles as it fills. Peak memory
use drops by up to half, and on Linux outputs of 10 to 32 MiB decompress up
to 1.5 times faster.
+ Fix ``python -m isal.igzip`` replacing the input file with empty output and
exiting with status 0 when the output file is the input file, as in
``-f -o data data``. It exits with an error now.
Expand Down
18 changes: 18 additions & 0 deletions src/isal/isal_shared.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ static PyObject *IsalError;
/* Initial buffer size. */
#define DEF_BUF_SIZE (16*1024)
#define DEF_MAX_INITIAL_BUF_SIZE (16 * 1024 * 1024)
/* Largest gzip ISIZE used to size one-shot output. */
#define DEF_MAX_ISIZE_HINT (32 * 1024 * 1024)
#define ISAL_BEST_SPEED ISAL_DEF_MIN_LEVEL
#define ISAL_BEST_COMPRESSION ISAL_DEF_MAX_LEVEL
#define ISAL_DEFAULT_COMPRESSION 2
Expand Down Expand Up @@ -408,6 +410,22 @@ igzip_lib_decompress_impl(Py_buffer *data, int flag,
} else if (bufsize == 0) {
bufsize = 1;
}
/* Size the output from ISIZE in the gzip trailer, plus one byte so that a
buffer the output fills exactly is not grown. ISIZE can be wrong
(trailing data, more members, corruption), so it is capped by
deflate's 1032:1 ratio and DEF_MAX_ISIZE_HINT. */
if ((flag == ISAL_GZIP || flag == ISAL_GZIP_NO_HDR_VER) &&
data->len >= 8) {
const uint8_t *trailer = (const uint8_t *)data->buf + data->len - 4;
uint32_t isize = (uint32_t)trailer[0] |
((uint32_t)trailer[1] << 8) |
((uint32_t)trailer[2] << 16) |
((uint32_t)trailer[3] << 24);
if (isize < DEF_MAX_ISIZE_HINT && (Py_ssize_t)isize >= bufsize &&
isize / 1032 <= (size_t)data->len) {
bufsize = (Py_ssize_t)isize + 1;
}
}

ibuf = (uint8_t *)data->buf;
ibuflen = data->len;
Expand Down
79 changes: 79 additions & 0 deletions tests/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import gzip
import itertools
import random
import struct
import subprocess
import sys
import textwrap
Expand All @@ -24,6 +25,11 @@

import pytest

try:
import tracemalloc
except ImportError: # PyPy
tracemalloc = None # type: ignore

DATA_DIR = Path(__file__).parent / "data"
COMPRESSED_FILE = DATA_DIR / "test.fastq.gz"
with gzip.open(str(COMPRESSED_FILE), mode="rb") as file_h:
Expand Down Expand Up @@ -139,6 +145,79 @@ def test_decompress_bufsize(module):
module.decompress(compressed, bufsize=-1)


GZIP_DECOMPRESS = [
pytest.param(isal_zlib, {"wbits": 31}, id="isal_zlib-wbits31"),
pytest.param(isal_zlib, {"wbits": 47}, id="isal_zlib-wbits47"),
pytest.param(igzip_lib, {"flag": igzip_lib.DECOMP_GZIP}, id="igzip_lib"),
]


@pytest.mark.parametrize(["module", "kwargs"], GZIP_DECOMPRESS)
@pytest.mark.parametrize("data_size", [0, 1, 16 * 1024 - 1, 16 * 1024,
16 * 1024 + 1, 1024 * 1024])
def test_decompress_gzip_bufsize(module, kwargs, data_size):
data = DATA[:data_size]
compressed = gzip.compress(data)
assert module.decompress(compressed, **kwargs) == data
for bufsize in [0, 1, max(data_size - 1, 0), data_size, data_size + 1,
2 * 1024 * 1024]:
assert module.decompress(compressed, **kwargs,
bufsize=bufsize) == data


@pytest.mark.parametrize(["module", "kwargs"], GZIP_DECOMPRESS)
@pytest.mark.parametrize("trailing_data", [
bytes(4), b"\xff\xff\xff\x01", b"\xff\xff\xff\xff",
gzip.compress(b"another member"), gzip.compress(bytes(1024 * 1024))],
ids=["zeros", "almost-32-MiB", "4-GiB", "small-member", "large-member"])
def test_decompress_gzip_trailing_data(module, kwargs, trailing_data):
data = DATA[:100_000]
compressed = gzip.compress(data) + trailing_data
assert zlib.decompress(compressed, wbits=31) == data
assert module.decompress(compressed, **kwargs) == data


@pytest.mark.parametrize(["module", "kwargs"], GZIP_DECOMPRESS)
@pytest.mark.parametrize("isize", [0, 99_999, 100_001, 2**24, 2**25 - 1,
2**32 - 1])
def test_decompress_gzip_wrong_isize(module, kwargs, isize):
compressed = gzip.compress(DATA[:100_000])
trailer = struct.pack("<I", isize)
# Wrong ISIZE, and truncated streams that end in a plausible ISIZE.
for corrupt in [compressed[:-4] + trailer, compressed[:-9] + trailer,
compressed[:len(compressed) // 2] + trailer]:
with pytest.raises(isal_zlib.error):
module.decompress(corrupt, **kwargs)


@pytest.mark.skipif(tracemalloc is None, reason="PyPy has no tracemalloc")
@pytest.mark.parametrize(["module", "kwargs"], GZIP_DECOMPRESS)
@pytest.mark.parametrize(["data_size", "isize"], [
# Deflate cannot expand 1000 bytes to almost 32 MiB.
(1000, 2**25 - 1),
# Sizes from 32 MiB up are not used.
(50_000, 2**25),
(50_000, 2**32 - 1),
])
def test_decompress_gzip_ignores_implausible_isize(module, kwargs, data_size,
isize):
# Incompressible, so the input is about as large as the output.
data = random.Random(data_size).randbytes(data_size)
compressed = gzip.compress(data) + struct.pack("<I", isize)
was_tracing = tracemalloc.is_tracing()
if not was_tracing:
tracemalloc.start()
try:
tracemalloc.reset_peak()
before = tracemalloc.get_traced_memory()[0]
assert module.decompress(compressed, **kwargs) == data
peak = tracemalloc.get_traced_memory()[1] - before
finally:
if not was_tracing:
tracemalloc.stop()
assert peak < 1024 * 1024


@pytest.mark.parametrize(["data_size", "level", "wbits", "memLevel"],
list(itertools.product([128 * 1024], range(4),
WBITS_RANGE, range(1, 10))))
Expand Down
Loading