From: Ian Rogers <irogers@google.com>
To: Peter Zijlstra <peterz@infradead.org>,
Ingo Molnar <mingo@redhat.com>,
Arnaldo Carvalho de Melo <acme@kernel.org>,
Namhyung Kim <namhyung@kernel.org>
Cc: Jiri Olsa <jolsa@kernel.org>,
Adrian Hunter <adrian.hunter@intel.com>,
James Clark <james.clark@linaro.org>,
Kan Liang <kan.liang@linux.intel.com>,
linux-perf-users@vger.kernel.org, linux-kernel@vger.kernel.org,
Ian Rogers <irogers@google.com>
Subject: [PATCH 07/10] perf jevents: Add --compress=zstd block compression backend
Date: Fri, 13 Nov 2026 13:08:19 -0800 [thread overview]
Message-ID: <20261113210822.3112700-8-irogers@google.com> (raw)
In-Reply-To: <20261113210822.3112700-1-irogers@google.com>
Add a 'zstd' block-indexed string compression backend to jevents.py that
trains a single shared 64 KiB Zstandard dictionary across all ~8 KiB
string blocks (itself stored zstd-compressed in .rodata and digested
once on demand into a shared ZSTD_DDict freed at exit) and compresses
each ~8 KiB block independently with ZSTD_compress_usingCDict().
Without a dictionary, each ~8 KiB block starts with an empty match
window and must emit its own Huffman/FSE entropy tables from scratch.
Sharing a trained 64 KiB dictionary pre-seeds both common event/metric
substrings and precomputed entropy tables across all blocks, shrinking
compressed .rodata by ~26% compared to dictionary-free ~8 KiB zstd
blocks (738 KiB vs 1,003 KiB across all architectures, recovering over
half the gap toward a single non-seekable 521 KiB whole-table stream)
while keeping every block independently decompressible via
(block_id << 16) | sub_offset.
To avoid mandatory third-party Python package dependencies across Python
versions, lazily probe three Zstandard bindings in order:
1. Python 3.14+ standard-library compression.zstd
2. python-zstandard (zstandard) if installed
3. ctypes binding to the system libzstd shared library
When total string data is too small to train a dictionary, gracefully
fall back to dictionary-free Zstandard frames. On x86, --compress=zstd
shrinks pmu-events-string.o from ~6.90 MB to ~587 KB (~11.8x reduction).
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/pmu-events/jevents.py | 266 +++++++++++++++++++++++++++++++
1 file changed, 266 insertions(+)
diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index a7fad9bbf7b4..b32344e7eb8e 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -281,12 +281,278 @@ def _compress_blocks_lzma(blocks: Sequence[bytes]) -> Optional[CompressedStringO
)
+def _zstd_compress_via_ctypes(
+ blocks: Sequence[bytes], dict_capacity: int, level: int
+) -> Optional[Tuple[Sequence[bytes], bytes, bytes]]:
+ """Compress blocks and dictionary via ctypes binding to libzstd."""
+ try:
+ ctypes = importlib.import_module('ctypes')
+ ctypes_util = importlib.import_module('ctypes.util')
+ lib_name = ctypes_util.find_library('zstd') or 'libzstd.so.1'
+ lib = ctypes.CDLL(lib_name)
+ except (ImportError, OSError):
+ return None
+
+ c_size_t = ctypes.c_size_t
+ c_void_p = ctypes.c_void_p
+ c_uint = ctypes.c_uint
+ c_int = ctypes.c_int
+
+ try:
+ lib.ZSTD_isError.argtypes = [c_size_t]
+ lib.ZSTD_isError.restype = c_uint
+ lib.ZDICT_isError.argtypes = [c_size_t]
+ lib.ZDICT_isError.restype = c_uint
+ lib.ZDICT_trainFromBuffer.argtypes = [
+ c_void_p, c_size_t, c_void_p, ctypes.POINTER(c_size_t), c_uint
+ ]
+ lib.ZDICT_trainFromBuffer.restype = c_size_t
+ lib.ZSTD_compressBound.argtypes = [c_size_t]
+ lib.ZSTD_compressBound.restype = c_size_t
+ lib.ZSTD_compress.argtypes = [c_void_p, c_size_t, c_void_p, c_size_t, c_int]
+ lib.ZSTD_compress.restype = c_size_t
+ lib.ZSTD_createCCtx.argtypes = []
+ lib.ZSTD_createCCtx.restype = c_void_p
+ lib.ZSTD_freeCCtx.argtypes = [c_void_p]
+ lib.ZSTD_freeCCtx.restype = c_size_t
+ lib.ZSTD_createCDict.argtypes = [c_void_p, c_size_t, c_int]
+ lib.ZSTD_createCDict.restype = c_void_p
+ lib.ZSTD_freeCDict.argtypes = [c_void_p]
+ lib.ZSTD_freeCDict.restype = c_size_t
+ lib.ZSTD_compress_usingCDict.argtypes = [
+ c_void_p, c_void_p, c_size_t, c_void_p, c_size_t, c_void_p
+ ]
+ lib.ZSTD_compress_usingCDict.restype = c_size_t
+ except AttributeError:
+ return None
+
+ raw_dict = b''
+ total_bytes = sum(len(b) for b in blocks)
+ if total_bytes >= 16384:
+ full_blob = b''.join(blocks)
+ sample_step = 2048
+ sample_lens = [
+ min(sample_step, len(full_blob) - off)
+ for off in range(0, len(full_blob), sample_step)
+ ]
+ if len(sample_lens) >= 8:
+ sizes_arr = (c_size_t * len(sample_lens))(*sample_lens)
+ dict_buf = ctypes.create_string_buffer(dict_capacity)
+ dsize = lib.ZDICT_trainFromBuffer(
+ dict_buf, dict_capacity, full_blob, sizes_arr, len(sample_lens)
+ )
+ if not lib.ZDICT_isError(dsize) and dsize > 0:
+ raw_dict = dict_buf.raw[:dsize]
+
+ cctx = lib.ZSTD_createCCtx()
+ if not cctx:
+ return None
+ cdict = None
+ try:
+ if raw_dict:
+ cdict = lib.ZSTD_createCDict(raw_dict, len(raw_dict), level)
+ if not cdict:
+ raw_dict = b''
+
+ max_block_len = max(len(b) for b in blocks)
+ dst_cap = lib.ZSTD_compressBound(max(max_block_len, len(raw_dict)))
+ dst_buf = ctypes.create_string_buffer(dst_cap)
+ compressed_blocks: list[bytes] = []
+
+ for block in blocks:
+ if cdict:
+ res = lib.ZSTD_compress_usingCDict(
+ cctx, dst_buf, dst_cap, block, len(block), cdict
+ )
+ else:
+ res = lib.ZSTD_compress(dst_buf, dst_cap, block, len(block), level)
+ if lib.ZSTD_isError(res):
+ return None
+ compressed_blocks.append(dst_buf.raw[:res])
+
+ comp_dict = b''
+ if raw_dict:
+ dres = lib.ZSTD_compress(dst_buf, dst_cap, raw_dict, len(raw_dict), level)
+ if lib.ZSTD_isError(dres):
+ return None
+ comp_dict = dst_buf.raw[:dres]
+
+ return compressed_blocks, raw_dict, comp_dict
+ finally:
+ if cdict:
+ lib.ZSTD_freeCDict(cdict)
+ lib.ZSTD_freeCCtx(cctx)
+
+
+def _compress_blocks_zstd(blocks: Sequence[bytes]) -> Optional[CompressedStringOutput]:
+ """Compress ~8 KiB blocks independently with zstd and a shared trained dictionary.
+
+ Trains a single 64 KiB Zstandard dictionary across all blocks (itself stored
+ zstd-compressed in .rodata and digested once on demand into a shared
+ ZSTD_DDict) so each ~8 KiB block achieves high compression ratios while
+ remaining independently decompressible via (block_id << 16) | sub_offset.
+ Probes Python 3.14+ compression.zstd, python-zstandard, or ctypes libzstd.
+ """
+ dict_capacity = 65536
+ level = 9
+ result: Optional[Tuple[Sequence[bytes], bytes, bytes]] = None
+
+ # 1. Try Python 3.14+ standard library compression.zstd.
+ try:
+ py_zstd = importlib.import_module('compression.zstd')
+ raw_dict = b''
+ zdict_obj = None
+ full_blob = b''.join(blocks)
+ if len(full_blob) >= 16384:
+ samples = [full_blob[i:i + 2048] for i in range(0, len(full_blob), 2048)]
+ try:
+ zdict_obj = py_zstd.train_dict(samples, dict_capacity)
+ raw_dict = bytes(zdict_obj.dict_content)
+ except Exception:
+ raw_dict = b''
+ zdict_obj = None
+ if zdict_obj is not None:
+ cblocks = [py_zstd.compress(b, level=level, zstd_dict=zdict_obj) for b in blocks]
+ cdict_bytes = py_zstd.compress(raw_dict, level=level)
+ else:
+ cblocks = [py_zstd.compress(b, level=level) for b in blocks]
+ cdict_bytes = b''
+ result = (cblocks, raw_dict, cdict_bytes)
+ except ImportError:
+ pass
+
+ # 2. Try third-party zstandard module if installed.
+ if result is None:
+ try:
+ zstd_pkg = importlib.import_module('zstandard')
+ raw_dict = b''
+ pkg_dict = None
+ full_blob = b''.join(blocks)
+ if len(full_blob) >= 16384:
+ samples = [full_blob[i:i + 2048] for i in range(0, len(full_blob), 2048)]
+ try:
+ pkg_dict = zstd_pkg.train_dictionary(dict_capacity, samples)
+ raw_dict = pkg_dict.as_bytes()
+ except Exception:
+ raw_dict = b''
+ pkg_dict = None
+ comp = zstd_pkg.ZstdCompressor(level=level, dict_data=pkg_dict)
+ cblocks = [comp.compress(b) for b in blocks]
+ cdict_bytes = (
+ zstd_pkg.ZstdCompressor(level=level).compress(raw_dict)
+ if raw_dict else b''
+ )
+ result = (cblocks, raw_dict, cdict_bytes)
+ except ImportError:
+ pass
+
+ # 3. Fall back to ctypes binding to system libzstd.
+ if result is None:
+ result = _zstd_compress_via_ctypes(blocks, dict_capacity, level)
+
+ if result is None:
+ return None
+
+ compressed_blocks, raw_dict, comp_dict = result
+ if raw_dict:
+ dict_lines = [
+ f'#define BIG_C_STRING_ZDICT_RAW_LEN {len(raw_dict)}U\n',
+ f'#define BIG_C_STRING_ZDICT_COMP_LEN {len(comp_dict)}U\n',
+ '/* clang-format off */\n',
+ 'static const unsigned char big_c_string_zdict[] =\n',
+ ]
+ for i in range(0, len(comp_dict), 64):
+ chunk = comp_dict[i:i + 64]
+ escaped = ''.join(_BYTE_TO_C_OCTAL[b] for b in chunk)
+ dict_lines.append(f'\t"{escaped}"\n')
+ dict_lines.append(';\n/* clang-format on */\n\n')
+ dict_lines.append('static ZSTD_DDict *big_c_string_ddict;\n')
+ extra_c_decls = ''.join(dict_lines)
+
+ decompress_fn = """static const ZSTD_DDict *get_zstd_ddict(void)
+{
+\tZSTD_DDict *ddict = __atomic_load_n(&big_c_string_ddict, __ATOMIC_ACQUIRE);
+
+\tif (!ddict) {
+\t\tZSTD_DDict *expected = NULL;
+\t\tvoid *dict_buf = malloc(BIG_C_STRING_ZDICT_RAW_LEN);
+
+\t\tif (!dict_buf)
+\t\t\treturn NULL;
+\t\tif (ZSTD_decompress(dict_buf, BIG_C_STRING_ZDICT_RAW_LEN,
+\t\t\t\t big_c_string_zdict,
+\t\t\t\t BIG_C_STRING_ZDICT_COMP_LEN) !=
+\t\t BIG_C_STRING_ZDICT_RAW_LEN) {
+\t\t\tfree(dict_buf);
+\t\t\treturn NULL;
+\t\t}
+\t\tddict = ZSTD_createDDict(dict_buf, BIG_C_STRING_ZDICT_RAW_LEN);
+\t\tfree(dict_buf);
+\t\tif (!ddict)
+\t\t\treturn NULL;
+\t\tif (!__atomic_compare_exchange_n(&big_c_string_ddict, &expected,
+\t\t\t\t\t\t ddict, false,
+\t\t\t\t\t\t __ATOMIC_RELEASE,
+\t\t\t\t\t\t __ATOMIC_ACQUIRE)) {
+\t\t\tZSTD_freeDDict(ddict);
+\t\t\tddict = expected;
+\t\t}
+\t}
+\treturn ddict;
+}
+
+static bool decompress_block(const unsigned char *src, uint32_t src_len,
+\t\t\t char *dst, uint32_t dst_len)
+{
+\tconst ZSTD_DDict *ddict = get_zstd_ddict();
+\tZSTD_DCtx *dctx;
+\tsize_t ret;
+
+\tif (!ddict)
+\t\treturn false;
+\tdctx = ZSTD_createDCtx();
+\tif (!dctx)
+\t\treturn false;
+\tret = ZSTD_decompress_usingDDict(dctx, dst, dst_len, src, src_len, ddict);
+\tZSTD_freeDCtx(dctx);
+\treturn !ZSTD_isError(ret) && ret == dst_len;
+}
+"""
+ cleanup_stmt = (
+ '\tif (big_c_string_ddict) {\n'
+ '\t\tZSTD_freeDDict(big_c_string_ddict);\n'
+ '\t\tbig_c_string_ddict = NULL;\n'
+ '\t}\n'
+ )
+ else:
+ extra_c_decls = ''
+ decompress_fn = """static bool decompress_block(const unsigned char *src, uint32_t src_len,
+\t\t\t char *dst, uint32_t dst_len)
+{
+\tsize_t ret = ZSTD_decompress(dst, dst_len, src, src_len);
+
+\treturn !ZSTD_isError(ret) && ret == dst_len;
+}
+"""
+ cleanup_stmt = ''
+
+ return CompressedStringOutput(
+ name='zstd',
+ c_headers=['<zstd.h>'],
+ compressed_blocks=compressed_blocks,
+ extra_c_decls=extra_c_decls,
+ decompress_c_fn=decompress_fn,
+ cleanup_c_stmt=cleanup_stmt,
+ )
+
+
_COMPRESS_BACKENDS: Dict[
str, Optional[Callable[[Sequence[bytes]], Optional[CompressedStringOutput]]]
] = {
'none': None,
'lzma': _compress_blocks_lzma,
'zlib': _compress_blocks_zlib,
+ 'zstd': _compress_blocks_zstd,
}
--
2.56.1.703.gf848ed807c-goog
next prev parent reply other threads:[~2026-11-13 21:08 UTC|newest]
Thread overview: 21+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-13 21:08 [PATCH 00/10] perf jevents: Block-indexed string compression and default JEVENTS_ARCH=all Ian Rogers
2026-11-13 21:08 ` [PATCH 01/10] perf jevents: Store raw bytes in BigCString instead of C-escaped strings Ian Rogers
2026-11-13 21:21 ` sashiko-bot
2026-11-13 21:08 ` [PATCH 02/10] perf jevents: Sort BigCString entries alphabetically Ian Rogers
2026-11-13 21:12 ` sashiko-bot
2026-11-13 21:08 ` [PATCH 03/10] perf jevents: Store PMU names as string literals in struct pmu_table_entry Ian Rogers
2026-11-13 21:13 ` sashiko-bot
2026-11-13 21:08 ` [PATCH 04/10] perf jevents: Encapsulate big_c_string lookups behind pmu_events__get_string() Ian Rogers
2026-11-13 21:12 ` sashiko-bot
2026-11-13 21:08 ` [PATCH 05/10] perf jevents: Add block-indexed string packing and --compress=zlib Ian Rogers
2026-11-13 21:16 ` sashiko-bot
2026-11-13 21:08 ` [PATCH 06/10] perf jevents: Add --compress=lzma block compression backend Ian Rogers
2026-11-13 21:13 ` sashiko-bot
2026-11-13 21:08 ` Ian Rogers [this message]
2026-11-13 21:13 ` [PATCH 07/10] perf jevents: Add --compress=zstd " sashiko-bot
2026-11-13 21:08 ` [PATCH 08/10] perf build: Enable jevents string compression when available Ian Rogers
2026-11-13 21:17 ` sashiko-bot
2026-11-13 21:08 ` [PATCH 09/10] perf tests pmu-events: Test metricgroup descriptions and concurrent lookups Ian Rogers
2026-11-13 21:20 ` sashiko-bot
2026-11-13 21:08 ` [PATCH 10/10] perf build: Default JEVENTS_ARCH to all Ian Rogers
2026-11-13 21:21 ` sashiko-bot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261113210822.3112700-8-irogers@google.com \
--to=irogers@google.com \
--cc=acme@kernel.org \
--cc=adrian.hunter@intel.com \
--cc=james.clark@linaro.org \
--cc=jolsa@kernel.org \
--cc=kan.liang@linux.intel.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-perf-users@vger.kernel.org \
--cc=mingo@redhat.com \
--cc=namhyung@kernel.org \
--cc=peterz@infradead.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox