From 6f6a27874d47b4f83ff3ce3a858d4ac71959ba12 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sat, 31 Oct 2026 11:46:02 +0000 Subject: [PATCH 1/5] perf(deps): bump cachekit-core to 0.7.4 Per-thread LZ4 table reuse for 64 B to under 4 KiB stores. Moves the four hand-maintained core pins with the root lock, and documents the per-thread memory the table reuse retains. --- Cargo.lock | 8 ++++---- docs/features/rust-serialization.md | 2 +- docs/free-threading.md | 2 +- rust/fuzz/Cargo.lock | 8 ++++---- rust/fuzz/Cargo.toml | 2 +- tests/unit/protocol/test_envelope_wire_vectors.py | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48729219..64962685 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,9 +198,9 @@ checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "cachekit-core" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "101672176881ef05b17e8399476eb6fc2082e6a5c33788c84c9bb4d1a8d512d7" +checksum = "d9217fe47d4024d7b0d1dda460bbcfc0f613bd80fb94c4fbbc4e7e6d829d7342" dependencies = [ "aes", "aes-gcm", @@ -892,9 +892,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lz4_flex" -version = "0.12.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90071f8077f8e40adfc4b7fe9cd495ce316263f19e75c2211eeff3fdf475a3d9" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" [[package]] name = "memchr" diff --git a/docs/features/rust-serialization.md b/docs/features/rust-serialization.md index e51a7f64..c8590410 100644 --- a/docs/features/rust-serialization.md +++ b/docs/features/rust-serialization.md @@ -51,7 +51,7 @@ LZ4 is chosen for its speed/ratio balance: | Already-compressed data | ~1x (negligible overhead) | | Random bytes | ~1x | -ByteStorage LZ4 compression of serialized envelopes (MessagePack: StandardSerializer and AutoSerializer) runs automatically whenever integrity checking is on (the default; `@cache.secure` forces it on for a serializer named by string, while a serializer instance you pass keeps its own setting); it has no separate switch. A payload under 64 bytes is not run through the compressor: ByteStorage writes it as one uncompressed LZ4 literal block, which any LZ4 decoder reads, so a small value is stored at its own size plus the envelope's overhead. Arrow (DataFrame) payloads use a separate Arrow IPC codec, selectable via the `CACHEKIT_ARROW_COMPRESSION` environment variable (`zstd`, `lz4`, or `none`) — that setting applies only to Arrow payloads and does not disable ByteStorage LZ4 for other serializers. +ByteStorage LZ4 compression of serialized envelopes (MessagePack: StandardSerializer and AutoSerializer) runs automatically whenever integrity checking is on (the default; `@cache.secure` forces it on for a serializer named by string, while a serializer instance you pass keeps its own setting); it has no separate switch. A payload under 64 bytes is not run through the compressor: ByteStorage writes it as one uncompressed LZ4 literal block, which any LZ4 decoder reads, so a small value is stored at its own size plus the envelope's overhead. From 64 bytes to under 4 KiB, ByteStorage compresses with one LZ4 hash table and one output scratch kept per thread instead of allocating them on every call, so each thread that stores such a value retains about 12.5 KiB (an 8 KiB table and a 4.4 KiB scratch) for its lifetime; the compressed bytes are the same either way. Arrow (DataFrame) payloads use a separate Arrow IPC codec, selectable via the `CACHEKIT_ARROW_COMPRESSION` environment variable (`zstd`, `lz4`, or `none`) — that setting applies only to Arrow payloads and does not disable ByteStorage LZ4 for other serializers. --- diff --git a/docs/free-threading.md b/docs/free-threading.md index f275607c..e1f5e21f 100644 --- a/docs/free-threading.md +++ b/docs/free-threading.md @@ -78,7 +78,7 @@ covered, plus what the free-threaded CI lane surfaced and sites added since: | `reliability/async_metrics.py` `AsyncMetricsCollector.record_cache_operation` + `_ThreadState` + `_Shards` + `_ShardView` | Every backed cache operation counts once per collector and updates three Prometheus series. It used to write a shared `_operation_count` (unlocked, so lossy with the GIL off) and either the series' value locks or the batching queue | **Fixed** — each thread writes only its own `_ThreadState`, held in a `threading.local`: one cell per series it records into, and one operation count per collector. The series' values are `_ShardView`s that sum the live cells plus an exact retired total, read under one per-PID lock that no record takes, so a scrape and `get_stats()` count every operation recorded before them. Freed at thread exit, a `_ThreadState` moves its counts into the retired totals and drops its cells in one locked step, so reads never decrease and cells stay bounded to the live threads; sums are retired as exact partials (`math.fsum`), so a retirement never re-rounds a total one ulp lower, which `rate()` would read as a counter reset (an infinite term collapses the sum to `inf`). The lock is reentrant, because a garbage collection under it can run a finalizer on the same thread. A count move such a finalizer starts there is deferred to the next section that is not nested. One example is a cyclic collection freeing a thread's state that a traceback kept alive past the exit, so no read sees a count half moved. The at-fork hooks hold the lock across `os.fork`; a forked child starts from the counts it inherited. In prometheus_client's multiprocess mode cache operations record into the series directly. Tests in `tests/unit/test_async_metrics_per_thread.py` | | `backends/memcached/backend.py` `MemcachedBackend` | pymemcache `HashClient(use_pooling=True)`: one `PooledClient` per server, whose `ObjectPool` `threading.Lock` guards only the free/used deque pop and append, never socket I/O | Safe across threads: each operation owns its checked-out connection, so no socket is shared. The pool raises at `max_pool_size` rather than waiting (`tests/unit/backends/test_memcached_pooling.py`). pymemcache's failover state (`HashClient._failed_clients`) is unlocked: while a failed server is retried, concurrent operations can raise a spurious `BackendError` wrapping `KeyError` (documented in `docs/backends/memcached.md#concurrency`). Not fork-safe, by design: a child that reused its parent's backend would share the parent's pooled sockets, and could deadlock on a pool lock a parent thread held at fork. A per-PID lock would cure only the deadlock, not the shared sockets, so the rule is that a forked child builds its own backend (documented in `docs/backends/memcached.md#concurrency`) | | `reliability/async_metrics.py` `AsyncMetricsCollector._maybe_switch_mode`, `shutdown`, `_take_over_if_forked` | Auto sync/batched switch; `_should_check_mode` is an unlocked check-then-set, so two producers can both run a switch. A forked child inherits the batching state (queue, stop event, worker, pool lock) and prometheus_client's metric locks | **Fixed** — one `Lock` held across the switch, so concurrent switches start one worker. The lock is per PID (a per-instance dict filled with `setdefault`, like `_metrics_locks`), so a child forked while a thread is mid-switch never waits on the copy that thread still holds. `shutdown()` takes the same lock, bars any later switch from starting a worker, and sets sync mode before stopping the worker, so every later record lands synchronously; a record another thread had already chosen to queue can miss the worker's final drain (at most one per such thread). Returning to batched mode starts a worker on the existing queue only once the previous worker has exited (its exit drain assumes a single consumer); while it is alive the collector stays synchronous and retries at the next mode check, never joining on the caller's thread. Within a process the queue is never replaced, so producers always find one. **Fork:** an owner-PID take-over gives an inherited collector fresh batching state and sync mode in a forked child. The at-fork hook (after a fork made from C, the first take-over or collector built in the child instead) gives cachekit's own metrics and the default registry fresh prometheus_client locks; an application's own metrics are not covered, and neither are values in prometheus_client's multiprocess mode (`PROMETHEUS_MULTIPROC_DIR`), which share one lock the reset cannot reach, so a child forked while a parent thread holds it can still hang. No collector, inherited or built there, starts a thread in the child of a fork made from C, where at-fork hooks are skipped and `is_alive()` stays stale, nor in a child that first imports cachekit after such a fork of a parent that ran threads (uWSGI `--lazy-apps`; the detection of the `l1_cache.py` row). On free-threaded builds such a fork without `--py-call-uwsgi-fork-hooks` is unsupported: any Python code in that child can hang on a per-object lock a parent thread held, before cachekit can act. The `_take_over_if_forked`, `_reset_metric_locks` and `_reset_metric_locks_once` docstrings give the mechanics and the open cases. Cache operations never queue, so only generic metrics and mode checks touch the batching state. There is no per-record `getpid()` on the sync path. Tests in `tests/unit/test_async_metrics_mode_switch.py` | -| Rust extension (`rust/src/`, cachekit-core 0.7.2) | `#[pymodule(gil_used = false)]`; every `#[pyclass]` exposes only `&self` methods; the nonce counters (the process-wide instance counter and each encryptor's own), core's only shared state in the features cachekit-py enables, are `AtomicU64`; PyO3 enforces `Send + Sync` on pyclasses at compile time | Safe — declared free-threading-ready. (The wasm32 `Cell` nonce variant is single-threaded by target.) | +| Rust extension (`rust/src/`, cachekit-core 0.7.4) | `#[pymodule(gil_used = false)]`; every `#[pyclass]` exposes only `&self` methods; the nonce counters (the process-wide instance counter and each encryptor's own), core's only shared state in the features cachekit-py enables, are `AtomicU64` (core's LZ4 table and scratch are thread-local, so no thread reads another's); PyO3 enforces `Send + Sync` on pyclasses at compile time | Safe — declared free-threading-ready. (The wasm32 `Cell` nonce variant is single-threaded by target.) | ## The CI safety net diff --git a/rust/fuzz/Cargo.lock b/rust/fuzz/Cargo.lock index d931d351..cb992e31 100644 --- a/rust/fuzz/Cargo.lock +++ b/rust/fuzz/Cargo.lock @@ -71,9 +71,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "cachekit-core" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "101672176881ef05b17e8399476eb6fc2082e6a5c33788c84c9bb4d1a8d512d7" +checksum = "d9217fe47d4024d7b0d1dda460bbcfc0f613bd80fb94c4fbbc4e7e6d829d7342" dependencies = [ "aes", "aes-gcm", @@ -321,9 +321,9 @@ dependencies = [ [[package]] name = "lz4_flex" -version = "0.12.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90071f8077f8e40adfc4b7fe9cd495ce316263f19e75c2211eeff3fdf475a3d9" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" [[package]] name = "num-traits" diff --git a/rust/fuzz/Cargo.toml b/rust/fuzz/Cargo.toml index 5c3eef16..5c0a3af3 100644 --- a/rust/fuzz/Cargo.toml +++ b/rust/fuzz/Cargo.toml @@ -22,7 +22,7 @@ rmp-serde = "1" # Bump it in the same diff as every parent-crate core bump. [dependencies.cachekit_storage] package = "cachekit-core" -version = "=0.7.2" +version = "=0.7.4" features = ["compression", "checksum", "messagepack", "encryption"] # Prevent this from interfering with normal build diff --git a/tests/unit/protocol/test_envelope_wire_vectors.py b/tests/unit/protocol/test_envelope_wire_vectors.py index 4c01df3f..44b84041 100644 --- a/tests/unit/protocol/test_envelope_wire_vectors.py +++ b/tests/unit/protocol/test_envelope_wire_vectors.py @@ -73,7 +73,7 @@ CARGO_LOCK = Path(__file__).parents[3] / "Cargo.lock" # The cachekit-core release whose publish job ran core's allocation probe (docstring item 6). -PROBE_VERIFIED_CORE_VERSION = "0.7.2" +PROBE_VERIFIED_CORE_VERSION = "0.7.4" FIXTURE_PATH = Path(__file__).parent / "fixtures" / "wire-format.json" FIXTURE_SHA256 = "806de79dca2e0e6b221b8ec191526207d32f9fe5d88597098dd2549427b4993c" # pragma: allowlist secret From a098fdd9807d15ada04783cce53f6c2133cdd98c Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 1 Nov 2026 01:37:30 +0000 Subject: [PATCH 2/5] perf(ir): re-record ByteStorage budgets for cachekit-core 0.7.4 The 40 B store rises by about 33 Ir/op: core's new size-class branch and Result return change what fat LTO inlines into the store call. The small-dict serializer round trips, which now reuse the per-thread LZ4 table, fall by 1.2 to 1.7%. --- docs/performance.md | 8 ++++---- tests/performance/ir_baselines.json | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/performance.md b/docs/performance.md index 04aa348b..dd2e4dc1 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -304,15 +304,15 @@ The regression gate is the instruction budget: `make perf-ir` on your machine, a | `miss` | `@cache`, L1 disabled, L2 miss, compute, L2 write, all as one in-process single-flight trip | 211,434 | 203,521 | | `secure_l1_hit` | `@cache.secure` L1 hit (builds its key, decrypts the ciphertext L1 holds) | 139,943 | 140,926 | | `l2_hit_async_metrics` | `l2_hit` with the metrics collector in batched mode | 171,370 | 167,447 | -| `serializer_default` | `StandardSerializer` round trip, small dict | 58,661 | 59,595 | +| `serializer_default` | `StandardSerializer` round trip, small dict | 57,670 | 58,612 | | `serializer_default_records` | `StandardSerializer` round trip, list of 100 six-field records (dict-heavy decode) | 1,422,079 | 1,422,120 | -| `serializer_auto` | `AutoSerializer` round trip | 65,971 | 66,624 | +| `serializer_auto` | `AutoSerializer` round trip | 64,882 | 65,642 | | `serializer_orjson` | `OrjsonSerializer` round trip | 22,085 | 22,677 | | `serializer_arrow` | `ArrowSerializer` round trip, 100-row DataFrame | 1,972,713 | 1,982,226 | | `serializer_arrow_usgs` | `ArrowSerializer` round trip, real 12,535-row DataFrame (see Real data below) | 61,750,733 | 61,321,806 | | `serializer_auto_usgs` | `AutoSerializer` round trip, same frame (it hands the frame to Arrow) | 61,806,031 | 61,327,994 | -| `serializer_encrypted` | `EncryptionWrapper` encrypt + decrypt round trip | 100,164 | 101,927 | -| `byte_storage_store_40b` | `ByteStorage.store()` alone, 40-byte compressible input (under cachekit-core's 64-byte literals-only LZ4 threshold) | 2,658 | 2,530 | +| `serializer_encrypted` | `EncryptionWrapper` encrypt + decrypt round trip | 98,999 | 101,927 | +| `byte_storage_store_40b` | `ByteStorage.store()` alone, 40-byte compressible input (under cachekit-core's 64-byte literals-only LZ4 threshold) | 2,690 | 2,566 | | `byte_storage_store_64k` | `ByteStorage.store()` alone, 64 KiB of address-like text | 720,667 | 720,663 | | `file_set` | `FileBackend.set()` overwriting one key in a 1,000-entry cache (no eviction) | 75,887 | 74,132 | | `object_cache_put_full` | `ObjectCache.put()` of a new key into a full 10,000-entry byte budget (evicts the LRU entry; nothing expired) | 164,514 | 160,511 | diff --git a/tests/performance/ir_baselines.json b/tests/performance/ir_baselines.json index 18a191f0..c1d60328 100644 --- a/tests/performance/ir_baselines.json +++ b/tests/performance/ir_baselines.json @@ -2,7 +2,7 @@ "interpreters": { "cpython-3.12-x86_64": { "budgets": { - "byte_storage_store_40b": 2658, + "byte_storage_store_40b": 2690, "byte_storage_store_64k": 720667, "file_set": 75887, "l1_hit": 2594, @@ -14,18 +14,18 @@ "secure_l1_hit": 139943, "serializer_arrow": 1972713, "serializer_arrow_usgs": 61750733, - "serializer_auto": 65971, + "serializer_auto": 64882, "serializer_auto_usgs": 61806031, - "serializer_default": 58661, + "serializer_default": 57670, "serializer_default_records": 1422079, - "serializer_encrypted": 100164, + "serializer_encrypted": 98999, "serializer_orjson": 22085 }, "python": "3.12.12" }, "cpython-3.14-x86_64": { "budgets": { - "byte_storage_store_40b": 2530, + "byte_storage_store_40b": 2566, "byte_storage_store_64k": 720663, "file_set": 74132, "l1_hit": 2482, @@ -37,9 +37,9 @@ "secure_l1_hit": 140926, "serializer_arrow": 1982226, "serializer_arrow_usgs": 61321806, - "serializer_auto": 66624, + "serializer_auto": 65642, "serializer_auto_usgs": 61327994, - "serializer_default": 59595, + "serializer_default": 58612, "serializer_default_records": 1422120, "serializer_encrypted": 101927, "serializer_orjson": 22677 From 2ebc98cd7ccec20618cf4c8ebcf01dfa00b71af1 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 1 Nov 2026 02:30:11 +0000 Subject: [PATCH 3/5] perf(ir): re-record cachekit-core 0.7.4 budgets on main's workload main re-recorded every budget after an ir_workload.py change, which moves the heap layout the budgets hold at, so the four ByteStorage paths this bump moves are re-measured on the merged tree with --update --allow-increase on CPython 3.12.12 and 3.14.3. --- docs/performance.md | 8 ++++---- tests/performance/ir_baselines.json | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/performance.md b/docs/performance.md index b21d96bb..bbf66fd6 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -311,15 +311,15 @@ The regression gate is the instruction budget: `make perf-ir` on your machine, a | `miss` | `@cache`, L1 disabled, L2 miss, compute, L2 write, all as one in-process single-flight trip | 213,364 | 204,897 | | `secure_l1_hit` | `@cache.secure` L1 hit (builds its key, decrypts the ciphertext L1 holds) | 140,428 | 141,391 | | `l2_hit_async_metrics` | `l2_hit` with the metrics collector in batched mode | 171,976 | 167,728 | -| `serializer_default` | `StandardSerializer` round trip, small dict | 58,733 | 59,574 | +| `serializer_default` | `StandardSerializer` round trip, small dict | 57,744 | 58,608 | | `serializer_default_records` | `StandardSerializer` round trip, list of 100 six-field records (dict-heavy decode) | 1,420,388 | 1,424,292 | -| `serializer_auto` | `AutoSerializer` round trip | 65,942 | 66,628 | +| `serializer_auto` | `AutoSerializer` round trip | 64,722 | 65,653 | | `serializer_orjson` | `OrjsonSerializer` round trip | 22,135 | 22,825 | | `serializer_arrow` | `ArrowSerializer` round trip, 100-row DataFrame | 1,969,626 | 1,980,777 | | `serializer_arrow_usgs` | `ArrowSerializer` round trip, real 12,535-row DataFrame (see Real data below) | 61,781,406 | 61,324,948 | | `serializer_auto_usgs` | `AutoSerializer` round trip, same frame (it hands the frame to Arrow) | 61,793,577 | 61,344,459 | -| `serializer_encrypted` | `EncryptionWrapper` encrypt + decrypt round trip | 100,097 | 101,938 | -| `byte_storage_store_40b` | `ByteStorage.store()` alone, 40-byte compressible input (under cachekit-core's 64-byte literals-only LZ4 threshold) | 2,658 | 2,540 | +| `serializer_encrypted` | `EncryptionWrapper` encrypt + decrypt round trip | 99,162 | 100,937 | +| `byte_storage_store_40b` | `ByteStorage.store()` alone, 40-byte compressible input (under cachekit-core's 64-byte literals-only LZ4 threshold) | 2,688 | 2,558 | | `byte_storage_store_64k` | `ByteStorage.store()` alone, 64 KiB of address-like text | 720,631 | 720,751 | | `file_set` | `FileBackend.set()` overwriting one key in a 1,000-entry cache (no eviction) | 76,035 | 74,112 | | `object_cache_put_full` | `ObjectCache.put()` of a new key into a full 10,000-entry byte budget (evicts the LRU entry; nothing expired) | 165,014 | 160,608 | diff --git a/tests/performance/ir_baselines.json b/tests/performance/ir_baselines.json index 7d8ce9a1..7192456d 100644 --- a/tests/performance/ir_baselines.json +++ b/tests/performance/ir_baselines.json @@ -2,7 +2,7 @@ "interpreters": { "cpython-3.12-x86_64": { "budgets": { - "byte_storage_store_40b": 2658, + "byte_storage_store_40b": 2688, "byte_storage_store_64k": 720631, "file_set": 76035, "l1_hit": 2596, @@ -15,18 +15,18 @@ "secure_l1_hit": 140428, "serializer_arrow": 1969626, "serializer_arrow_usgs": 61781406, - "serializer_auto": 65942, + "serializer_auto": 64722, "serializer_auto_usgs": 61793577, - "serializer_default": 58733, + "serializer_default": 57744, "serializer_default_records": 1420388, - "serializer_encrypted": 100097, + "serializer_encrypted": 99162, "serializer_orjson": 22135 }, "python": "3.12.12" }, "cpython-3.14-x86_64": { "budgets": { - "byte_storage_store_40b": 2540, + "byte_storage_store_40b": 2558, "byte_storage_store_64k": 720751, "file_set": 74112, "l1_hit": 2482, @@ -39,11 +39,11 @@ "secure_l1_hit": 141391, "serializer_arrow": 1980777, "serializer_arrow_usgs": 61324948, - "serializer_auto": 66628, + "serializer_auto": 65653, "serializer_auto_usgs": 61344459, - "serializer_default": 59574, + "serializer_default": 58608, "serializer_default_records": 1424292, - "serializer_encrypted": 101938, + "serializer_encrypted": 100937, "serializer_orjson": 22825 }, "python": "3.14.3" From 5c71d5296870b9856e2fc3f5875efebd38a1fde5 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 1 Nov 2026 02:51:08 +0000 Subject: [PATCH 4/5] perf(ir): ratchet the decode-path budgets on the merged tree main's Rust decode lowered serializer_default by 17% and serializer_encrypted by 10% without moving their budgets; both run through ByteStorage, so they are lowered to what this tree measures on CPython 3.12.12 and 3.14.3. --- docs/performance.md | 4 ++-- tests/performance/ir_baselines.json | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/performance.md b/docs/performance.md index 10408b0c..1f03df61 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -313,14 +313,14 @@ The regression gate is the instruction budget: `make perf-ir` on your machine, a | `miss` | `@cache`, L1 disabled, L2 miss, compute, L2 write, all as one in-process single-flight trip | 213,364 | 204,897 | | `secure_l1_hit` | `@cache.secure` L1 hit (builds its key, decrypts the ciphertext L1 holds) | 140,428 | 141,391 | | `l2_hit_async_metrics` | `l2_hit` with the metrics collector in batched mode | 171,976 | 167,728 | -| `serializer_default` | `StandardSerializer` round trip, small dict | 57,744 | 58,608 | +| `serializer_default` | `StandardSerializer` round trip, small dict | 47,895 | 48,623 | | `serializer_default_records` | `StandardSerializer` round trip, list of 100 six-field records (dict-heavy decode) | 1,420,388 | 1,424,292 | | `serializer_auto` | `AutoSerializer` round trip | 64,722 | 65,653 | | `serializer_orjson` | `OrjsonSerializer` round trip | 22,135 | 22,825 | | `serializer_arrow` | `ArrowSerializer` round trip, 100-row DataFrame | 1,969,626 | 1,980,777 | | `serializer_arrow_usgs` | `ArrowSerializer` round trip, real 12,535-row DataFrame (see Real data below) | 61,781,406 | 61,324,948 | | `serializer_auto_usgs` | `AutoSerializer` round trip, same frame (it hands the frame to Arrow) | 61,793,577 | 61,344,459 | -| `serializer_encrypted` | `EncryptionWrapper` encrypt + decrypt round trip | 99,162 | 100,937 | +| `serializer_encrypted` | `EncryptionWrapper` encrypt + decrypt round trip | 89,289 | 90,918 | | `byte_storage_store_40b` | `ByteStorage.store()` alone, 40-byte compressible input (under cachekit-core's 64-byte literals-only LZ4 threshold) | 2,688 | 2,558 | | `byte_storage_store_64k` | `ByteStorage.store()` alone, 64 KiB of address-like text | 720,631 | 720,751 | | `file_set` | `FileBackend.set()` overwriting one key in a 1,000-entry cache (no eviction) | 76,035 | 74,112 | diff --git a/tests/performance/ir_baselines.json b/tests/performance/ir_baselines.json index 7192456d..6a4af3fc 100644 --- a/tests/performance/ir_baselines.json +++ b/tests/performance/ir_baselines.json @@ -17,9 +17,9 @@ "serializer_arrow_usgs": 61781406, "serializer_auto": 64722, "serializer_auto_usgs": 61793577, - "serializer_default": 57744, + "serializer_default": 47895, "serializer_default_records": 1420388, - "serializer_encrypted": 99162, + "serializer_encrypted": 89289, "serializer_orjson": 22135 }, "python": "3.12.12" @@ -41,9 +41,9 @@ "serializer_arrow_usgs": 61324948, "serializer_auto": 65653, "serializer_auto_usgs": 61344459, - "serializer_default": 58608, + "serializer_default": 48623, "serializer_default_records": 1424292, - "serializer_encrypted": 100937, + "serializer_encrypted": 90918, "serializer_orjson": 22825 }, "python": "3.14.3" From 0e8cd851c999d729603689a016bc08e7671e178d Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 1 Nov 2026 10:18:07 +0000 Subject: [PATCH 5/5] perf(ir): re-record byte_storage_store_40b at 300,000 calls on core 0.7.4 main now runs this path over 300,000 calls, which holds its figure within a few instructions, so both budgets come from the gate's own --update --allow-increase on CPython 3.12.12 and 3.14.3. --- docs/performance.md | 2 +- tests/performance/ir_baselines.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/performance.md b/docs/performance.md index c89c5da7..a8980313 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -334,7 +334,7 @@ The regression gate is the instruction budget: `make perf-ir` on your machine, a | `serializer_arrow_usgs` | `ArrowSerializer` round trip, real 12,535-row DataFrame (see Real data below) | 61,781,406 | 61,324,948 | | `serializer_auto_usgs` | `AutoSerializer` round trip, same frame (it hands the frame to Arrow) | 61,793,577 | 61,344,459 | | `serializer_encrypted` | `EncryptionWrapper` encrypt + decrypt round trip | 89,289 | 90,918 | -| `byte_storage_store_40b` | `ByteStorage.store()` alone, 40-byte compressible input (under cachekit-core's 64-byte literals-only LZ4 threshold) | 2,656 | 2,532 | +| `byte_storage_store_40b` | `ByteStorage.store()` alone, 40-byte compressible input (under cachekit-core's 64-byte literals-only LZ4 threshold) | 2,690 | 2,565 | | `byte_storage_store_64k` | `ByteStorage.store()` alone, 64 KiB of address-like text | 720,631 | 720,751 | | `file_set` | `FileBackend.set()` overwriting one key in a 1,000-entry cache (no eviction) | 76,035 | 74,112 | | `object_cache_put_full` | `ObjectCache.put()` of a new key into a full 10,000-entry byte budget (evicts the LRU entry; nothing expired) | 165,014 | 160,608 | diff --git a/tests/performance/ir_baselines.json b/tests/performance/ir_baselines.json index 95c32ff9..c5a21aee 100644 --- a/tests/performance/ir_baselines.json +++ b/tests/performance/ir_baselines.json @@ -2,7 +2,7 @@ "interpreters": { "cpython-3.12-x86_64": { "budgets": { - "byte_storage_store_40b": 2656, + "byte_storage_store_40b": 2690, "byte_storage_store_64k": 720631, "file_set": 76035, "l1_hit": 2595, @@ -26,7 +26,7 @@ }, "cpython-3.14-x86_64": { "budgets": { - "byte_storage_store_40b": 2532, + "byte_storage_store_40b": 2565, "byte_storage_store_64k": 720751, "file_set": 74112, "l1_hit": 2487,