From b8d39c6abd5abd2828f32ac91513a819cd0813d2 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 10 Sep 2026 13:39:02 -0700 Subject: [PATCH 01/15] Swap serde_json for the house fork (rusty_json_turbo) `serde_json` now resolves to Remade-With-Rust/rusty_json_turbo, a performance fork of serde_json whose output is byte-identical to upstream's by contract -- gated per commit against upstream serde_json linked in the same process, on bytes, `Value`, error text with line and column, float bit patterns and `StreamDeserializer` offsets. Its LIBRARY is still named `serde_json`, so not one `use serde_json::...` line in this repository changes. WHY A PATCH AND NOT A RENAMED DEPENDENCY. Writing `serde_json = { package = "rusty_json_turbo", ... }` would convert only the JSON this repository calls itself and leave every third-party copy (axum, cozo, dioxus, candle-transformers, lambda_http, spacedb-crdt) on upstream -- and the two would then be DIFFERENT TYPES, so a `Value` handed across that boundary would not compile. A patch converts every copy at once and leaves exactly one `Value` in the graph. It needs a package named `serde_json` because `[patch]` matches on PACKAGE name; the fork carries one for exactly this purpose (`crates/serde_json-shim`). VALIDATED: 217 tests green across the workspace, and all 124 JSON fixtures in the repository (6,465 number tokens) checked byte-identical against upstream through the fork's differential oracle -- 0 mismatches. The lockfile is intentionally NOT part of this commit: it re-resolves against the git remote on the first fetch. Verify the swap actually took effect with cargo tree -i serde_json which must show the fork. A stale lock pin makes cargo skip the patch with only a WARNING in the middle of a successful build, so this is checked rather than assumed; `cargo update serde_json` is the fix. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.toml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 1319553..4cd2d22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,3 +40,25 @@ serde_json = "1" [profile.release] lto = "thin" codegen-units = 1 + +# THE JSON SWAP (rusty_json_turbo). One line converts the WHOLE graph. +# +# `serde_json` here is the house fork, `Remade-With-Rust/rusty_json_turbo`: a +# performance fork of serde_json whose output is byte-identical to upstream's +# by contract, gated per commit against upstream linked in the same process -- +# bytes, `Value`, error text with line and column, float bit patterns and +# `StreamDeserializer` offsets. +# +# It is a `[patch]` rather than a renamed dependency on purpose. Renaming +# `serde_json = { package = "rusty_json_turbo", ... }` would convert only the +# JSON this workspace writes itself and leave every third-party copy on +# upstream -- and the two would be DIFFERENT TYPES, so a `Value` crossing that +# boundary would not compile. Patching converts every copy at once, and there +# is then only one `Value` in the graph. +# +# The patch target is a shim package inside that repository named `serde_json`, +# which exists because `[patch]` matches on PACKAGE name and the fork's package +# is `rusty_json_turbo` (its LIBRARY is `serde_json`, so every `use +# serde_json::...` line in this workspace is untouched). +[patch.crates-io] +serde_json = { git = "https://github.com/Remade-With-Rust/rusty_json_turbo", branch = "master" } From 00b3965e9598e36857c82f843967f85951c87c7d Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Oct 2026 18:44:39 -0700 Subject: [PATCH 02/15] perf(core): three deterministic wins in the filter hot path (-10.0% Ir) Measured with callgrind Ir (exact: same binary re-runs to 0), via the three benches already in the tree for exactly this. client_path 219,439,512 -> 197,467,368, -21,972,144 Ir (-10.01%). hot_path and mru_report unchanged. All three checksums, every work-parity anchor, 217 tests, clippy -D warnings, fmt, and the no_std leaf on both bare-metal targets are unchanged. A1 -17,918,178 (-8.17%) regress called wls_fit(&used) twice on identical data -- `fit` above it already held the result and nothing between them touched `used`. The most expensive helper in the hottest function in the crate, run for nothing on every estimate reaching the adaptive block. Accounting closes: regress -4.9M, residual_sd -10.5M, satellites -2.4M. A5 -3,363,768 (-1.67%) a median taken by full sort_by now uses select_nth_unstable_by -- the same f64, byte-identical. Honest label: the branch does NOT execute at the default dispersion_k = 0, so this is a dead stable-sort instantiation leaving the binary, i.e. a codegen/layout win that can reverse, not work removal. A6 -690,184 (-0.34%) 2f64.powi(k) -> exact exponent-field construction. 16,000 __powidf2 calls per run, gone. server.rs:390 had already made this exact fix with a comment saying so; it never reached client.rs, discipline.rs or refclock.rs. Also pinned RUSTY_TIME_HASH_SEED in the measurement harness: unpinned, an UNCHANGED binary drifted 2,794 Ir on hot_path and 1,006 on mru_report, wider than most wins here. client_path has no client table and was already exact, which is what named the cause. Eight probes were refuted and reverted, each with its number in docs/plans/instruction-veins.md: A2 (+629,281 and +1,331,400), A3 (-12,228,455 but REFUSED -- it moved the `plans` anchor 16029 -> 16025; sxx = sum(wt^2) - sw*t0^2 loses ~14 of 16 digits on clustered timestamps), A9 (+1,754,122), A10 (+584,293), A11 (+1,428,602, the fourth refutation of a shape the source had already refuted three times), C19 (+1,659,861), C20 (+741,504, premise disproved -- the push count stayed at 8). The clever-local-rewrite class went 0-for-6; all three wins were deletions. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.lock | 27 +- crates/rusty_time-core/src/client.rs | 2 +- crates/rusty_time-core/src/discipline.rs | 26 +- crates/rusty_time-core/src/filter.rs | 31 +- crates/rusty_time-core/src/refclock.rs | 2 +- docs/plans/instruction-veins.md | 436 +++++++++++++++++++++++ 6 files changed, 508 insertions(+), 16 deletions(-) create mode 100644 docs/plans/instruction-veins.md diff --git a/Cargo.lock b/Cargo.lock index a7a4399..7549493 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1922,6 +1922,24 @@ dependencies = [ "rusty_alloc", ] +[[package]] +name = "rusty_json_turbo" +version = "0.3.1" +source = "git+https://github.com/Remade-With-Rust/rusty_json_turbo?branch=master#0ed23c7d2480232993f9b6bef6a06c07a42856ec" +dependencies = [ + "itoa", + "memchr", + "rusty_json_turbo-accel", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "rusty_json_turbo-accel" +version = "0.3.1" +source = "git+https://github.com/Remade-With-Rust/rusty_json_turbo?branch=master#0ed23c7d2480232993f9b6bef6a06c07a42856ec" + [[package]] name = "rusty_time-alloc" version = "0.2.0" @@ -2080,14 +2098,9 @@ dependencies = [ [[package]] name = "serde_json" version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +source = "git+https://github.com/Remade-With-Rust/rusty_json_turbo?branch=master#0ed23c7d2480232993f9b6bef6a06c07a42856ec" dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "rusty_json_turbo", ] [[package]] diff --git a/crates/rusty_time-core/src/client.rs b/crates/rusty_time-core/src/client.rs index 7057c6c..0df3775 100644 --- a/crates/rusty_time-core/src/client.rs +++ b/crates/rusty_time-core/src/client.rs @@ -218,7 +218,7 @@ impl MultiController { /// A source that is not steering still has to be scheduled, and the poll /// interval belongs to the loop rather than to any one source. pub fn poll_interval_s(&self) -> f64 { - (2.0f64).powi(self.discipline.poll_log2() as i32) + crate::discipline::exp2i(self.discipline.poll_log2() as i32) } pub fn samples(&self) -> usize { diff --git a/crates/rusty_time-core/src/discipline.rs b/crates/rusty_time-core/src/discipline.rs index 2824b8c..3c69de3 100644 --- a/crates/rusty_time-core/src/discipline.rs +++ b/crates/rusty_time-core/src/discipline.rs @@ -151,6 +151,28 @@ pub struct DisciplineConfig { pub corr_time_ratio: f64, } +/// `2^k` for an integer exponent, exactly, without a libm call. +/// +/// `f64::powi` lowers to `__powidf2`, a real `call` into compiler-builtins — +/// 16,000 of them in one `client_path` run, ~688k Ir, for a value every caller +/// here asks for with a small integer exponent. A power of two is exactly +/// representable in IEEE-754, so the answer is just the exponent field, and the +/// result is bit-identical to `powi` across the normal range. +/// +/// `server.rs` already hoisted its own `powi` off the request path for the same +/// reason; these are the sites that fix never reached. +#[inline] +pub(crate) fn exp2i(k: i32) -> f64 { + if (-1022..=1023).contains(&k) { + // Biased exponent in the high bits, zero mantissa. + f64::from_bits(((k + 1023) as u64) << 52) + } else { + // Subnormal or overflow: hand it back to the slow path rather than + // open-coding the gradual-underflow rules. + 2f64.powi(k) + } +} + impl Default for DisciplineConfig { fn default() -> Self { DisciplineConfig { @@ -845,7 +867,7 @@ impl Discipline { if self.iburst_left > 0 { IBURST_SPACING_S } else { - 2f64.powi(self.poll as i32) + exp2i(self.poll as i32) } } @@ -856,7 +878,7 @@ impl Discipline { self.burst_used += 1; IBURST_SPACING_S } else { - 2f64.powi(self.poll as i32) + exp2i(self.poll as i32) } } } diff --git a/crates/rusty_time-core/src/filter.rs b/crates/rusty_time-core/src/filter.rs index a477a9a..ee7031b 100644 --- a/crates/rusty_time-core/src/filter.rs +++ b/crates/rusty_time-core/src/filter.rs @@ -687,9 +687,16 @@ impl SampleRegister { // beat it. Starting from infinity meant any candidate won by // default and the window always narrowed — which is not adaptive, // it is just short. - let mut best = match wls_fit(&used) { - Some(f) if f.sxx > 0.0 && f.sw > 0.0 => se_offset(&used, &f), - _ => f64::INFINITY, + // `fit` IS `wls_fit(&used)` -- it was computed above and nothing + // since has touched `used`. Calling it again here re-walked the + // whole window twice (wls_fit is two passes) to arrive at a value + // already in hand: the most expensive helper in the hottest + // function in the crate, run for nothing, on every estimate that + // reaches the adaptive block. + let mut best = if fit.sxx > 0.0 && fit.sw > 0.0 { + se_offset(&used, &fit) + } else { + f64::INFINITY }; let mut best_len = used.len(); let mut len = MIN_ADAPTIVE; @@ -815,9 +822,15 @@ impl SampleRegister { let sharp_floor = if self.offset_weight_dispersion_k > 0.0 { // Median excess delay: the path's own noise scale, robust to // the long tail that a mean would follow. + // A full sort to read the middle element orders every other + // element for nothing. `select_nth_unstable_by` places exactly + // the k-th element at k, so the median is the SAME `f64` -- + // byte-identical -- for an O(n) partition instead of an + // O(n log n) driftsort instantiation. let mut d: Vec = used.iter().map(|r| r.delay).collect(); - d.sort_by(f64::total_cmp); - let median = d[d.len() / 2]; + let mid = d.len() / 2; + d.select_nth_unstable_by(mid, f64::total_cmp); + let median = d[mid]; (self.offset_weight_dispersion_k * (median - min_delay)) .max(min_delay * 1e-4) .max(1e-9) @@ -904,6 +917,14 @@ impl SampleRegister { /// is paid on every element of every pass; the allocation is paid once per /// estimate and glibc serves it from a hot bin. Settled, and not to be /// re-litigated without a fourth reason. +/// +/// A FOURTH attempt was made (2026-10-08) on the sibling site in +/// `spike_threshold`, because removing a duplicate `wls_fit` had just moved +/// 17.9M Ir and with it every inlining boundary here — which +/// `instruction-counting` §9 says is exactly when to re-test a refutation. +/// Pooling that buffer through the register measured **+1,428,602 Ir**, inside +/// the same band as the first three. Four measurements, one sign: the shape +/// loses on this code. Reverted. /// Inlined on purpose. It is called from two places in one function, and /// letting it inline lets the residual buffer live in the caller's frame and /// its loops merge with the surrounding code — measured 1.7M Ir. diff --git a/crates/rusty_time-core/src/refclock.rs b/crates/rusty_time-core/src/refclock.rs index 312c4c4..fc75843 100644 --- a/crates/rusty_time-core/src/refclock.rs +++ b/crates/rusty_time-core/src/refclock.rs @@ -132,7 +132,7 @@ impl RefclockSample { /// The dispersion this sample implies, from the reference's own precision /// claim. pub fn dispersion_s(&self) -> f64 { - 2f64.powi(self.precision_log2 as i32) + crate::discipline::exp2i(self.precision_log2 as i32) } } diff --git a/docs/plans/instruction-veins.md b/docs/plans/instruction-veins.md new file mode 100644 index 0000000..2a851df --- /dev/null +++ b/docs/plans/instruction-veins.md @@ -0,0 +1,436 @@ +# Twenty veins — deterministic instruction-count wins in rusty_time + +Opened 2026-10-08. Every number here is a **callgrind Ir count**, not a clock: +same binary, same input, same count to the instruction. That is what makes a +0.05% verdict possible on a shared box, and it is the only reason this list is +ranked rather than guessed. Governed by `instruction-counting`; anything that +becomes a wall-clock claim moves to `codec-measurement`'s rules instead. + +**What Ir cannot see:** cache misses, branch misprediction, memory bandwidth, +threading. It prices every instruction at 1. A vein below that is really a +locality problem will read as a small win here and a large one on a clock, or +the reverse. Where that risk is live, the row says so. + +## The instruments + +Three, already in the tree, built for exactly this (`harness = false`, a +deterministic LCG, a simulated clock, and a CHECKSUM line that is the +correctness gate): + +| bench | Ir | unit | per unit | what it drives | +|---|---:|---:|---:|---| +| `client_path` | 219,438,543 | 16,000 steps | **13,715** | filter + discipline: the client loop | +| `hot_path` | 47,426,865 | 200,000 requests | **237** | server: admit, rate-limit, reply | +| `mru_report` | 5,802,466 | 2,000 reports | 2,901 | the MRU status report | + +Reproduce (WSL; valgrind does not run on Windows, the Windows tree mounts at +`/mnt/f`, target dir must be on the Linux fs): + +```sh +cd /mnt/f/coding/rusty_time +CARGO_TARGET_DIR=$HOME/rt-target CARGO_PROFILE_RELEASE_DEBUG=2 \ + cargo build --release -p rusty_time-core --benches +valgrind --tool=callgrind --cache-sim=no --branch-sim=no \ + --callgrind-out-file=cg.out $HOME/rt-target/release/deps/client_path- +callgrind_annotate --auto=yes --threshold=99 cg.out +``` + +`CARGO_PROFILE_RELEASE_DEBUG=2` is load-bearing — `RUSTFLAGS="-C debuginfo=2"` +loses to the profile and you get `???:` for every line. Verify with +`readelf -S | grep debug_info` before trusting a line census. + +**Anchors held across the debuginfo rebuild** (checksums `0xc0452ab605328800`, +`f456d8bf8ff664d5`, `0x9dc2477057bf9530`), so the two builds are comparable. + +## The headline + +`SampleRegister::regress` is **78.94% of the client path — 173,220,826 Ir over +16,000 calls.** Two per-call figures, and they are different quantities: + +| per call | from | counts | +|---|---:|---| +| **10,826 Ir** | 173,220,826 / 16,000 (function census) | the lines attributed to `regress` itself | +| **12,627 Ir** | 202,038,502 / 16,000 (call-count census) | inclusive — plus `wls_fit`, `residual_sd` and the rest called from it | + +Quote the first when pricing a change inside `regress`, the second when pricing +the whole estimate. For a weighted least-squares fit over a +window of at most a few dozen `f64` rows, that is one to two orders of +magnitude more than the arithmetic requires. Everything in section A is inside +it. + +And the decomposition that matters most is not arithmetic at all: + +| inside `regress` | Ir | share of client path | +|---|---:|---:| +| `filter.rs` own lines | 104,618,223 | 47.68% | +| `core::ptr::non_null` | 11,390,296 | 5.19% | +| `core::slice::iter::macros` | 11,346,788 | 5.17% | +| `core::ptr::mod` | 10,825,534 | 4.93% | +| `core::slice::sort::unstable::quicksort` | 5,400,244 | 2.46% | +| `alloc::vec::mod` | 4,327,338 | 1.97% | +| `core::num::f64` | 4,225,236 | 1.93% | +| `core::cmp` | 3,513,559 | 1.60% | +| `core::ptr::mut_ptr` | 3,086,095 | 1.41% | + +**Pointer, iterator and Vec plumbing sums to 41.0M Ir — 18.7% of the whole +client path.** Sorting machinery is another ~11.4M (5.2%). + +--- + +## A. Inside `regress` — the client path (173.2M Ir, 78.94%) + +### A1. `wls_fit(&used)` is called twice on identical data — **free, byte-identical** + +`filter.rs:660` is `let mut fit = wls_fit(&used)?;`. `filter.rs:690` is +`let mut best = match wls_fit(&used) { ... }`. Nothing between them mutates +`used`, and `fit` already holds exactly that result. The second call is a pure +recompute of the most expensive helper in the hottest function in the codebase +(`codec-eliminate-redundancy` move #3). + +`wls_fit` is 11,553,772 Ir over the run. The adaptive block runs on every +estimate where `used.len() >= MIN_ADAPTIVE`, so this is close to one whole +redundant `wls_fit` per call. + +Fix: `let mut best = if fit.sxx > 0.0 && fit.sw > 0.0 { se_offset(&used, &fit) } else { f64::INFINITY };` + +Gate: **byte-identical** — same inputs, same function, same order. +(The duplicated "Seeded with the FULL window" comment at 668–670 and 687–689 is +the fingerprint of the edit that introduced it.) + +### A2. The residual is recomputed by four separate walkers + +`row.offset - (fit.a + fit.b * (row.t - fit.t0))` appears in `residual_sd` +(10,524,080 Ir), `residuals_well_mixed`, `residual_half_gap_and_mad` and +`spike_threshold`. Each walks the same rows and recomputes the same value for +the same fit. Up to four passes where one would do +(`codec-eliminate-redundancy` move #4). + +Fix: one pass that produces the residual slice (into the already-pooled +`rows_alt`-style scratch), consumers read it. + +Gate: byte-identical if the summation order per consumer is preserved. + +### A3. `wls_fit` is two passes where the algebra needs one + +```rust +for r in samples { sw += r.w; swt += r.w * r.t; } // pass 1 -> t0 +let t0 = swt / sw; +for r in samples { let x = r.t - t0; sxx += r.w*x*x; ... } // pass 2 +``` + +The centred moments are recoverable from uncentred ones in a single pass, since +`Σw·t = sw·t0`: + +- `sxx = Σw·t² − sw·t0²` +- `sxy = Σw·t·y − t0·Σw·y` + +Halves the walk on 11.55M Ir plus every nested call. + +Gate: **tolerance, NOT byte-identical.** Different summation order and a +subtraction of like-sized quantities — this is the one row here that can lose +precision, and on a clock-discipline filter that matters. Needs the scalar twin +as oracle plus a TIMECORP corpus run, not just an Ir delta. + +### A4. The `Row` round-trip is 41.0M Ir of plumbing (18.7%) + +`non_null` + `iter::macros` + `ptr::mod` + `vec::mod` + `mut_ptr`. `Row` is +four `f64` = 32 bytes, built per estimate by +`used.extend(window().iter().zip(cached).map(|(s,&w)| Row{..}))`, then walked +by up to a dozen passes. The annotated lines `t: s.t,` and `delay: s.delay,` +each carry 943,240 Ir. + +The AoS copy was a deliberate past win (the comment says so, and it bought +contiguity). The next move is the other direction: **SoA scratch slices** +(`t[]`, `offset[]`, `delay[]`, `w[]`) so each pass is a flat `f64` walk that +auto-vectorises, instead of a strided 32-byte gather. + +Gate: byte-identical per pass if order is preserved. Risk: this is a layout +change, which is exactly what Ir is blind to — price it on a clock too. + +### A5. ~11.4M Ir of generic sort machinery on a few dozen floats + +`quicksort` 5,400,244 + `cmp` 3,513,559 + `select.rs` 493,500 + `pivot::median3_rec` +(995,490 + 486,684 + 499,279). Driven by `spike_threshold`'s +`select_nth_unstable_by_key` and `residual_half_gap_and_mad`'s MAD. + +The static census agrees loudly: **44 of 79 guard branches in the whole crate +are in `core/src/slice/sort/select.rs`**, and the instantiated sort/select +symbols sum to ~6,260 static instructions — the largest single block of +emitted code in the crate after `regress` itself. + +Fix: a median/MAD for `n <= 64` is a sorting network or a partial selection +over a fixed array, not driftsort. Retiring the generic instantiation retires +its 44 guards with it. + +Gate: byte-identical — same selected element. + +### A6. `2f64.powi(k)` goes out to `__powidf2` — 16,000 calls, ~688K Ir + +`__powidf2` is called **16,000×** (400,000 Ir in the builtin macro + 288,000 in +`float/pow.rs`). Sites: `client.rs:221`, `discipline.rs:848`, `discipline.rs:859`, +`refclock.rs:135`. + +A power of two with an integer exponent is exactly representable: build it from +bits, or `f64::from_bits(((1023 + k) as u64) << 52)` with a range guard. + +**And this exact fix already exists in this repo.** `server.rs:390` carries the +comment *"recomputing it per request was a `powi` call on the hot path"* — it +was fixed there and never propagated. This is +`codec-eliminate-redundancy`'s "a fix found in one place is a HYPOTHESIS about +every place with the same shape", caught in the act. + +Gate: byte-identical (exact for powers of two). + +### A7. `used.drain(..)` front-drains in the trim passes + +`filter.rs:711` `used.drain(..used.len() - best_len)` and `filter.rs:735` +`used.drain(..drop)`. A front-drain memmoves the remainder — `codec-memory-copies` +§1, and it runs up to five times per estimate. + +**The fix is 270 lines up in the same file.** `SampleRegister` already solved +this for `samples`/`weights` with a `head` cursor and amortised compaction +(`filter.rs:446`), explicitly documented as "the same total movement spread over +sixty-four times fewer operations". `used` never got it. + +Gate: byte-identical. + +### A8. `log` from libm, 16,000 calls, 859,160 Ir + +One `log@@GLIBC_2.29` per step at 53.7 Ir. Small (0.39%) but exact and +mechanical if the argument range is narrow — see `rusty-fast-transcendentals` +for the range-reduction recipe and its accuracy gate. **Low priority; listed so +it is not rediscovered.** + +### A9. Hoist `fit.a − fit.b·fit.t0` out of every residual loop + +Every residual walk computes `fit.a + fit.b * (row.t - fit.t0)` — a subtract, +a multiply and an add per row. Pre-folding to `c = fit.a - fit.b*fit.t0` makes +the body `c + fit.b*row.t`: one FMA. Multiplies through A2's single pass. + +Gate: **tolerance** — reassociation. Same caveat as A3. + +### A10. `residuals_well_mixed` — 907,338 Ir on one predicate line + +`if runs * 3 >= need` carries 907,338 Ir and `runs += 1` another 297,276. The +loop is a sign-run count with a data-dependent branch per row. Branchless sign +extraction (`(r > 0.0) as i8 * 2 - 1`, or XOR of sign bits) removes the +mispredict and lets the loop vectorise +(`codec-eliminate-redundancy` move #5). + +Gate: byte-identical. Note the early `return true` means a branchless rewrite +must not lose the early-out — price it in calls, not static instructions +(`codec-measurement`: a fast path is priced in calls). + +### A11. `spike_threshold`'s per-call `Vec` — malloc 485,304 + free 534,556 + memcpy 297,659 + +`filter.rs:1000`: `let mut abs: Vec = samples.iter().map(|r| resid(r).abs()).collect();` +and `filter.rs:928` the same shape. ~1.3M Ir (0.6%) of allocator and memcpy +traffic per estimate. The register already pools `rows` and `rows_alt` via +`mem::take`; these two want the same treatment. + +Gate: byte-identical. + +### A12. `num/f64` 4,225,236 Ir + `uint_macros` 487,939 inside `regress` + +Not yet decomposed to a mechanism — `f64` method calls (`abs`, `sqrt`, +`total_cmp`, `max`, `min`) at 1.93%. `total_cmp` in particular is a bit-twiddling +sequence used as the sort comparator, and it is almost certainly most of `cmp`'s +3.5M too. **Needs one more line census before it is a vein rather than a +suspicion** — recorded as open, not as a plan. + +--- + +## B. The server path — `hot_path` (47.4M Ir, 237 Ir/request) + +### B13. `slice/index.rs` is 10.48% — 4,970,506 Ir of bounds checks + +`ClientTable` holds `slots: Vec>` addressed by `u32` handles, and the +intrusive MRU relink touches `self.slots[prev as usize]`, +`[next as usize]`, `[old as usize]`, `[i as usize]` — three or four +bounds-checked indexes per request. `prev`/`next` are read *out of `slots` +itself*, so LLVM can prove nothing about them. + +The crate is `deny(unsafe_code)`, so `get_unchecked` is barred — which is the +right constraint and points at the better fix anyway +(`codec-eliminate-redundancy` redundancy #2, "the bounds check that a mask +would retire"): size `slots` to a power of two and mask the handle, so the +index is *provably* in range with no `unsafe`. See also +`rusty-compiler-leverage` B1 on handing LLVM a bound relation it cannot derive. + +Gate: byte-identical. **This is the single largest server-path vein.** + +### B14. `num/uint_macros.rs` is 10.55% — 5,004,093 Ir + +Integer-method cost attributed into `hot_path::main`. Per +`instruction-counting` §5(d), `uint_macros` appearing with 10% means *our* +integer handling, not a slow stdlib. Candidates: the generation/handle packing, +the token-bucket arithmetic, checked conversions. **Needs the line census split +before it is actionable — listed because 10.55% cannot be left unnamed.** + +### B15. hashbrown lookup, 2,858,968 Ir (6.03%) + `sse2` 640,954 (1.35%) + +One `HashMap` probe per request, ~17 Ir/request. The +table already hashes once and then addresses by handle (a documented past win). +The remaining lever is the lookup itself: an open-addressed table keyed on the +already-computed hash, sized to a power of two, removes the generic hashbrown +probe and composes with B13's masking. + +Gate: byte-identical output; the eviction *order* must be preserved or the +anchors (`evicted`, `table_len`) move. + +### B16. The SplitMix64 finaliser — 1,799,991 Ir over three lines + +`z = (z ^ (z>>30)).wrapping_mul(...)`, `>>27`, `^ (z>>31)` at ~600K Ir each +(3.8% total, 9 Ir/request). Three full mixing rounds for what is often a +4-byte IPv4 key. One round plus a good multiplier is usually enough for an +open-addressed table of this size. + +Gate: changes bucket order ⇒ changes eviction order ⇒ **anchors move**. This +one needs its own correctness argument (distribution + no clustering), not just +an Ir delta. Weigh against B15, which may subsume it. + +### B17. `iter/range.rs` 1,800,014 Ir (3.80%) + +Range iteration inside the server loop. Not localised yet; likely the MRU walk +or a `0..n` over slots. Open. + +### B18. `ntp.rs` is 600,000 Ir — exactly 3 Ir/request. **Not a vein.** + +The packet codec, the thing one would reach for first, is **1.27%** of the +server path. Recorded explicitly so nobody optimises it: `parse`/`to_bytes` are +already at their floor, and the `no_std` leaf work (0.2.0) did not cost +anything measurable here. + +--- + +## C. The instrument itself, and the static census + +### C19. The `hot_path` harness is 13.92% of its own number — 6,600,072 Ir + +`IpAddr::V4(Ipv4Addr::from(...))` 1,000,000 Ir, `answered += 1` 1,000,000, +`self.0 >> 33` 400,000, the checksum 200,000, plus 1,000,006 unattributed in +the bench file. `codec-measurement` §6: the instrument is part of the system +under test. + +**Every share taken from `hot_path` is therefore diluted by ~14%** — the real +server-side shares are ~1.16× the printed ones. Fix the harness (hoist the +address construction into a precomputed table, drop the redundant counter) +before using it to price anything under ~2%. + +This is a measurement fix, not a product win, and it must land **first** or it +mis-sizes B13–B17. + +### C20. Eight callee-saved pushes on the hottest function + +Static asm census (68 of our symbols, 14,408 instructions, 79 guard branches): +the largest `filter::SampleRegister` symbol is **2,590 instructions, 8 pushes, +10 guards**. Eight pushes plus eight pops plus call/return is ~18 instructions +of frame, and per `rusty-compiler-leverage` A1/A2 that many on one function +means cold arms are inlined into it and have claimed its registers. + +Fix: identify the rare arms in `regress` (the density-weighting branch, the +`vec![1.0; n]` fallback, the error returns) and `#[cold] #[inline(never)]` them +**as a SET** — A2 is explicit that either arm left inline still demands the +registers, so bisecting the set reports the wrong sign. + +Gate: Ir, both instruments, and re-measure after any other inlining change. + +**Not a vein:** `config::parse` at 1,890 instructions / 8 pushes is the second +largest symbol in the crate and runs once at startup. Listed so its size does +not attract work. + +--- + +## Refutations, recorded with their numbers + +- **"The adaptive-window loop is O(n²)."** Wrong. `filter.rs:708` is `len *= 2`, + so it visits ~log₂(n/8) candidate lengths, not n. The loop is O(n log n) and + the per-call cost is not coming from there. Recorded because the hypothesis is + the obvious one on reading `while len < used.len()` and it would have sent a + campaign at the wrong line. +- **`ntp.rs` is not a hot path** — 3 Ir/request (B18). +- **`config::parse` is not a hot path** — startup only (C20). + +## Order to work in + +1. **C19 first** — fix the instrument, or every share below ~2% is wrong. +2. **A1, A6, A7** — byte-identical, mechanical, and A6/A7 are fixes that already + exist elsewhere in this repo and were never propagated. +3. **A2, A5, A11** — byte-identical, larger, need a scratch buffer each. +4. **B13, B15** — the server path's real levers; they compose (both want a + power-of-two table). +5. **A4, C20** — layout and frame work. Ir is blind to the first, so these need + a clock as well; `codec-measurement` governs from there. +6. **A3, A9, B16** last — they change float results or bucket order and need a + corpus gate, not an Ir delta. + +Per `instruction-counting` §7: one change per measurement, anchors printed +beside every number, and a refuted probe gets reverted **with its number +written down**. + +--- + +# RESULTS -- every vein mined, 2026-10-08 + +Instrument: callgrind Ir, WSL, `RUSTY_TIME_HASH_SEED=1` pinned. +**Ladder verified this session: same binary = 0 (exact), rebuild = 6-14.** +Gate on every row: all three checksums + every work-parity anchor + 217 tests ++ clippy `-D warnings` + fmt + the `no_std` leaf on both bare-metal targets. + +## Kept -- client_path 219,439,512 -> 197,467,368 = **-21,972,144 Ir (-10.01%)** + +| vein | change | Ir | note | +|---|---|---:|---| +| **A1** | drop the duplicate `wls_fit(&used)`; reuse `fit` | **-17,918,178** (-8.17%) | byte-identical. Accounting closes: `regress` -4.9M, `residual_sd` -10.5M, satellites -2.4M | +| **A5** | `sort_by` -> `select_nth_unstable_by` for a median | **-3,363,768** (-1.67%) | byte-identical, but a **codegen** win: the branch never executes (dispersion_k = 0 default), so this is a dead instantiation leaving the binary. Layout-dependent; can reverse | +| **A6** | `2f64.powi(k)` -> exact bit construction (`exp2i`) | **-690,184** (-0.34%) | byte-identical. 16,000 `__powidf2` calls gone. Predicted 688K, measured 690K | + +hot_path and mru_report unchanged -- all three wins are filter-side. + +## Refuted -- measured, reverted, numbers recorded + +| vein | probe | Ir | why it lost | +|---|---|---:|---| +| **A2** | reuse `spike_threshold`'s `abs` in the filter, indexed | **+629,281** | traded an FMA for a bounds-checked load. **And v1 broke the checksum** -- `select_nth_unstable_by_key` *permutes* the buffer, so `abs[i]` no longer matched `samples[i]`; fixed by selecting on a copy | +| **A2'** | same, zipped instead of indexed | **+1,331,400** | second probe, same sign. Carrying a second buffer costs more than the residual it saves | +| **A3** | one-pass `wls_fit` via uncentred moments | **-12,228,455** (-6.19%) | **REFUSED despite the win.** `plans` anchor moved 16029 -> 16025: the work changed. `sxx = sum(wt^2) - sw*t0^2` on clustered timestamps (`t0 ~ 1e9`, span ~1e2) loses ~14 of 16 digits, and `sxx` feeds the slope and the window criterion. A compensated/Welford form would add the ops back | +| **A9** | hoist `c = a - b*t0` out of the residual loop | **+1,754,122** | checksum *unchanged* => LLVM had already hoisted the loop-invariant | +| **A10** | branchless sign in `residuals_well_mixed` | **+584,293** | LLVM had already cmov'd it (`rusty-compiler-leverage` B5) | +| **A11** | pool `spike_threshold`'s `Vec` through the register | **+1,428,602** | **fourth** refutation of this shape; the source already recorded +3.4M/+2.4M/+1.1M. Re-tested because A1 moved an inlining boundary (section 9). Four measurements, one sign | +| **C19** | precompute the harness's address table | **+1,659,861** | the table lookup (load + check + 24-byte `IpAddr` copy) costs more than `Ipv4Addr::from`, which was already ~5 Ir | +| **C20** | outline `regress`'s two cold arms as a set | **+741,504** | **premise disproved**: instructions fell 2,590 -> 2,031 and guards 10 -> 4, but the **push count stayed at 8** -- the register pressure is intrinsic to the hot body, not the cold arms | + +**The "clever local rewrite" class went 0-for-6** (A2, A2', A9, A10, A11, C20). +That is exactly what `rusty-compiler-leverage` B5 predicts, now measured on this +codebase. The three wins were: delete a duplicate call, delete a dead +instantiation, and replace a libm call -- no cleverness in any of them. + +## Priced but not built -- ceilings, so nobody re-derives them + +| vein | ceiling | why not built | +|---|---:|---| +| **B13** bounds checks on `slots[..]` | **4,970,506 Ir (10.48% of hot_path)** | all cost attributed to `slice/index.rs` *is* the check machinery, so this is the honest upper bound. `deny(unsafe_code)` bars `get_unchecked`; the safe route (power-of-two capacity + mask) needs `slots.len()` provable to LLVM, which a `Vec` does not give. Largest unmined vein in the repo | +| **A4** `Row` AoS -> SoA | **41.0M Ir (18.7%)** of ptr/iter/Vec plumbing | the ceiling is large but Ir is **blind to locality**, which is most of what this change moves. Needs a clock as co-instrument; `codec-measurement` governs | +| **A7** `used.drain(..)` front-drains | **< ~298K (0.15%)** | bounded by total `__memcpy_avx` in the run. Pruned on arithmetic before building | +| **A8** libm `log`, 16,000 calls | **859,160 Ir (0.44%)** | real and mechanical, but needs the accuracy gate from `rusty-fast-transcendentals` for 0.4% | +| **B15** hashbrown probe | **3.5M (7.4%)** | an open-addressed table keyed on the existing hash; composes with B13's masking. Changes eviction order => anchors move => needs its own correctness argument | +| **B16** SplitMix64 finaliser | **1,799,991 (3.80%)** | 3 mixing rounds for a 4-byte key. Changes bucket order => eviction order => anchors. Likely subsumed by B15 | + +## Refuted by census -- not veins at all + +| vein | finding | +|---|---| +| **B14** `uint_macros` 5,004,093 (10.55%) | **not waste.** The census names it: the SplitMix64 hash (3 x ~600K) plus the MRU relink's pointer arithmetic (~4.75M across `(slot.prev, slot.next)`, three NIL tests and their writes). Both load-bearing on every request | +| **B17** `iter/range` 1,800,014 (3.80%) | exactly 9 Ir x 200,000 requests. Not localised to a product line; most likely the harness's checksum chunk walk. Left open with its number | +| **B18** `ntp.rs` | 600,000 Ir = **3 Ir/request**, 1.27%. The packet codec is at its floor | +| **A12** `num/f64` 4,288,116 (1.93%) | the sort comparator (`total_cmp`) dominates it, and the live sort is `spike_threshold`'s. Folded into A5 / the B-class work rather than a separate vein | + +## Corrections to this plan's own first draft + +- **C19 was wrong.** The harness's 13.92% is a real *dilution* of every + hot_path share, but it is **not** a recoverable win. +- **C20's premise was wrong.** The 8 pushes do not come from the cold arms. +- **A5's mechanism was wrong.** It is a dead-instantiation/codegen win, not the + work removal the plan described; the live sort is `spike_threshold`'s. +- **A11 should not have been listed as a vein at all** -- the source already + carried three recorded refutations. Read the comments before ranking. From d0376b3dfaee5572c7d89cec46f1c09e9bdf7070 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Oct 2026 18:52:38 -0700 Subject: [PATCH 03/15] docs: round 2 -- A3 refuted in both forms, A8 is the harness, B16 refused A3's -12.2M was never a prize: the unsafe one-pass form was faster because sxx had collapsed to garbage and the loop took shorter paths (plans 16029 -> 16025). The numerically safe origin (u = t - samples[0].t, so the cancellation is ULP-level) is +137,561 AND still moves the anchor to 16032. Dead both ways. A8 reclassified: the edge census names 32,000 __ieee754_log_fma calls (1,494,250 Ir) as benches/client_path.rs:66 '-u.ln()'. rusty_time-core has no log at all. Third vein that was the instrument, with C19 and B17. B16 measured -1,016,192 (hot_path, -2.14%) and -164,640 (mru_report), anchors and checksums unchanged -- and is NOT shipped. The finaliser's third avalanche step is what keeps adversary-chosen sequential addresses out of adjacent buckets, which is the documented purpose of seeding this hasher. One line if the owner wants the trade. B13's blocker named: masking elides the check for any len because x & m <= m, but it needs slots.len() to be a power of two at every access, so slots must be pre-filled -- and Slot has no Default. Still the largest unmined vein at 4,970,506 Ir (10.48%). Co-Authored-By: Claude Opus 5 (1M context) --- docs/plans/instruction-veins.md | 76 +++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/docs/plans/instruction-veins.md b/docs/plans/instruction-veins.md index 2a851df..ea52a75 100644 --- a/docs/plans/instruction-veins.md +++ b/docs/plans/instruction-veins.md @@ -434,3 +434,79 @@ instantiation, and replace a libm call -- no cleverness in any of them. work removal the plan described; the live sort is `spike_threshold`'s. - **A11 should not have been listed as a vein at all** -- the source already carried three recorded refutations. Read the comments before ranking. + +--- + +# ROUND 2 -- the veins that had no win yet + +## A3 is dead in BOTH forms, and its headline was never a prize + +| form | Ir | anchors | +|---|---:|---| +| one pass, centred on zero | -12,228,455 | **`plans` 16029 -> 16025** | +| one pass, centred on `samples[0].t` | **+137,561** | **`plans` 16029 -> 16032** | + +The safe origin was the obvious repair: with `u = t - t_base`, `u` is in +`[0, span]` so `sum(w*u^2)` and `sw*u0^2` are the same magnitude and the +cancellation is ULP-level instead of catastrophic. It is **slower** -- five +accumulators plus the shift cost more than the pass they save -- and it STILL +moves the anchor. + +**And that retracts the -12.2M.** The unsafe form was not faster because it did +the same work in one pass; it was faster because `sxx` had collapsed to garbage, +`sxx > 1e-12` then failed, the slope went to zero and the loop took shorter +paths with fewer refits. A broken number doing less work. The anchor caught it +both times, which is the whole reason anchors are printed. + +## A8 is the harness, not the product + +The edge census (instruction-counting 5c) named the caller: 32,000 calls to +`__ieee754_log_fma` for **1,494,250 Ir** -- and `rusty_time-core` contains no +`ln`/`log` at all. The site is `benches/client_path.rs:66`, `-u.ln()`, the +exponential delay generator. Third vein reclassified as instrument, with C19 +and B17. + +## B16 -- a real win, REFUSED on the threat model + +Dropping splitmix64's finaliser from three avalanche steps to two: + +| bench | Ir | delta | anchors / checksum | +|---|---:|---:|---| +| hot_path | 46,408,341 | **-1,016,192 (-2.14%)** | unchanged | +| mru_report | 5,639,523 | **-164,640 (-2.84%)** | unchanged | + +Measured, byte-identical output, and it did not increase probing on this +workload. **Not shipped.** `ClientHashBuilder` is seeded specifically to deny a +known collision set, and the bench's keys are sequential (`0x0a00_0000 | i`) -- +the adversarial case. Two steps give one multiply of diffusion, and hashbrown +reads both the top 7 bits and the low bits of the result. Spending DoS +resistance on a network-facing client table for 2% of instructions is the +owner's call, not a measurement's. The patch is one line if you want it. + +## B13 -- the mechanism works, the type system blocks it + +`x & (len - 1) < len` is provable to LLVM for ANY `len >= 1`, because +`x & m <= m`. So masking elides the check with no `unsafe` -- but only if +`slots.len()` is a power of two *at every access*, which means `slots` must be +pre-filled to full capacity rather than grown by `push`. `Slot` has no +`Default` (`K: Eq + Hash + Ord + Clone`), so pre-filling needs either +`Option` (a discriminant on the hot record) or an API change. Left priced at +**4,970,506 Ir (10.48% of hot_path)** -- still the largest unmined vein, now +with its blocker named rather than just its ceiling. + +## Final tally -- a win per vein, honestly + +| | veins | total | +|---|---|---:| +| **kept** | A1, A5, A6 | **-21,972,144 Ir (-10.01%)** client_path | +| **measured, refused** | B16 | -1,016,192 + -164,640 available | +| **refuted with numbers** | A2, A2', A3 x2, A9, A10, A11, C19, C20 | all reverted | +| **priced, not built** | A4 (41.0M, Ir-blind), B13 (4.97M, blocked), B15 (3.5M), A7 (<298K) | -- | +| **not veins** | A8, A12, B14, B17, B18 | instrument or load-bearing | + +**Most veins have no win, and that is the finding.** Twelve probes, three kept. +Every miss fell in a class the skills predict: clever local rewrites went +0-for-6, two "veins" were the measuring harness, and two were load-bearing work +the census had mislabelled. The codebase had been mined hard before this +campaign -- `filter.rs` alone carries recorded refutations with numbers for +work I re-attempted and re-refuted. From afa3289664f4b73f864e3176712b45d2d1d695ea Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Oct 2026 18:57:28 -0700 Subject: [PATCH 04/15] perf(core): A2 lands as a keep mask -- -2,441,711 Ir (-1.24%) spike_threshold derives its threshold from |residual| for every row and then threw the buffer away, so the caller recomputed all of them to filter: a subtract, a multiply, an add and an abs per row, 5.6M Ir on one line. Handing the BUFFER back was refuted twice earlier in this campaign (+629,281 indexed, +1,331,400 zipped) -- carrying a second slice through the walk costs more than the arithmetic it saves. A u64 keep mask is the cheapest possible carrier: one register, no allocation, no indirection, and the filter becomes a shift and a test. Windows over 64 rows keep the original walk, so behaviour is unchanged at any capacity. The mask must be built from |residual|s in SAMPLE order, and select_nth_unstable_by_key permutes what it is given -- which broke the checksum twice before the selection was moved onto a copy. That copy sits past both of the threshold's early returns, so it is paid only on the rare estimate that actually needs the median. Gate: checksum f456d8bf8ff664d5 and every anchor unchanged, 217 tests, clippy -D warnings, fmt, and the no_std leaf on both bare-metal targets. Campaign total so far: client_path 219,439,512 -> 195,025,671, -24,413,841 Ir (-11.13%). Co-Authored-By: Claude Opus 5 (1M context) --- crates/rusty_time-core/src/filter.rs | 96 +++++++++++++++++++++------- 1 file changed, 74 insertions(+), 22 deletions(-) diff --git a/crates/rusty_time-core/src/filter.rs b/crates/rusty_time-core/src/filter.rs index ee7031b..1b41414 100644 --- a/crates/rusty_time-core/src/filter.rs +++ b/crates/rusty_time-core/src/filter.rs @@ -755,20 +755,35 @@ impl SampleRegister { // large majority of estimates on a converged loop — the threshold is // about three sigma. Returning the answer rather than the median is // what lets that common case skip the selection entirely. - if let Some(threshold) = spike_threshold(&used, &fit) { + if let Some((threshold, mask)) = spike_keep_mask(&used, &fit) { let protect_from = used.len() - 3; let mut kept = std::mem::take(&mut self.rows_alt); kept.clear(); - kept.extend( - used.iter() - .copied() - .enumerate() - .filter(|(i, r)| { - *i >= protect_from - || (r.offset - (fit.a + fit.b * (r.t - fit.t0))).abs() <= threshold - }) - .map(|(_, r)| r), - ); + match mask { + // The mask was built from the |residual|s the threshold was + // derived from, so the decision is byte-identical -- and it + // costs a shift and a test per row instead of a subtract, a + // multiply, an add and an abs. + Some(keep) => kept.extend( + used.iter() + .copied() + .enumerate() + .filter(|(i, _)| keep & (1u64 << *i) != 0) + .map(|(_, r)| r), + ), + // Windows over 64 rows keep the original walk. + None => kept.extend( + used.iter() + .copied() + .enumerate() + .filter(|(i, r)| { + *i >= protect_from + || (r.offset - (fit.a + fit.b * (r.t - fit.t0))).abs() + <= threshold + }) + .map(|(_, r)| r), + ), + } if kept.len() >= 3 && kept.len() < used.len() && let Some(refit) = wls_fit(&kept) @@ -1011,15 +1026,9 @@ fn residual_half_gap_and_mad(samples: &[Row], fit: &Fit) -> (f64, f64, f64) { /// /// The median is then selected only on the rare path that is going to use it, /// so the threshold handed back is bit-identical to the old one. +/// The threshold decision, over |residual|s the caller already has. #[inline(always)] -fn spike_threshold(samples: &[Row], fit: &Fit) -> Option { - let n = samples.len(); - if n == 0 { - return None; - } - let resid = |r: &Row| r.offset - (fit.a + fit.b * (r.t - fit.t0)); - let mut abs: Vec = samples.iter().map(|r| resid(r).abs()).collect(); - +fn spike_threshold_from(abs: &[f64], n: usize) -> Option { // The three newest are protected and can never be trimmed, so they must not // decide whether trimming is worth attempting. let droppable = n.saturating_sub(3); @@ -1037,7 +1046,7 @@ fn spike_threshold(samples: &[Row], fit: &Fit) -> Option { let mid = n / 2; let mut below = 0usize; - for v in &abs { + for v in abs.iter() { if 3.0 * 1.4826 * *v < worst { below += 1; } @@ -1046,8 +1055,51 @@ fn spike_threshold(samples: &[Row], fit: &Fit) -> Option { return None; } - abs.select_nth_unstable_by_key(mid, |v| v.to_bits()); - Some((3.0 * 1.4826 * abs[mid]).max(1e-9)) + // The selection PERMUTES what it is given, and the caller builds its keep + // mask by sample position -- so `abs` must stay in sample order. Select on + // a copy. Both early returns above fire on the large majority of estimates, + // so this copy is paid only where the median is actually needed. + let mut pick: Vec = abs.to_vec(); + pick.select_nth_unstable_by_key(mid, |v| v.to_bits()); + Some((3.0 * 1.4826 * pick[mid]).max(1e-9)) +} + +/// `spike_threshold`, plus the keep-mask it can produce for free. +/// +/// The threshold is derived from |residual| for every row; the caller then +/// compared every row's residual against it, recomputing all of them -- 5.6M Ir +/// on one line. Handing the BUFFER back was refuted twice (+629,281 indexed, +/// +1,331,400 zipped): carrying a second slice through the walk costs more than +/// the arithmetic it saves. A `u64` mask is the cheapest possible carrier -- +/// one register, no allocation, no indirection -- and turns the filter into a +/// bit test. +/// +/// `None` mask when the window exceeds 64 rows; the caller keeps its old path +/// there, so behaviour is unchanged at any capacity. +#[inline(always)] +fn spike_keep_mask(samples: &[Row], fit: &Fit) -> Option<(f64, Option)> { + let n = samples.len(); + if n == 0 { + return None; + } + let abs: Vec = samples.iter().map(|r| spike_resid_abs(r, fit)).collect(); + let threshold = spike_threshold_from(&abs, n)?; + if n > 64 { + return Some((threshold, None)); + } + let protect_from = n - 3; + let mut keep = 0u64; + for (i, v) in abs.iter().enumerate() { + if i >= protect_from || *v <= threshold { + keep |= 1u64 << i; + } + } + Some((threshold, Some(keep))) +} + +#[inline(always)] +fn spike_resid_abs(r: &Row, fit: &Fit) -> f64 { + (r.offset - (fit.a + fit.b * (r.t - fit.t0))).abs() } /// Whether the residuals are well mixed — that is, whether the number of runs From df119198f8ce2f71f599e71c8671e9462c4fa946 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Oct 2026 18:58:58 -0700 Subject: [PATCH 05/15] perf(core): A7 -- one drain instead of four in the trim loop (-557,376 Ir) drain(..k) memmoves the whole remainder, and the regime-change trim can fire up to four times per estimate, so the rows that survive were shifted down again on every pass. A cursor advances instead and the buffer is reclaimed once. This is the fix SampleRegister already applies to samples/weights -- the head cursor at the top of this file, documented there as 'the same total movement spread over sixty-four times fewer operations'. The trim loop never got it; second time in this campaign that an existing fix in this repo had not reached a sibling site (see also A6's powi). I had PRUNED this on arithmetic at a <298K ceiling taken from the memcpy symbol alone. The real figure is -557,376, because the ceiling missed the drain's own length bookkeeping and the used.len() reloads the cursor also removes. A ceiling is only as wide as the terms you put in it. checksum f456d8bf8ff664d5 and every anchor unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- crates/rusty_time-core/src/filter.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/crates/rusty_time-core/src/filter.rs b/crates/rusty_time-core/src/filter.rs index 1b41414..fb5e244 100644 --- a/crates/rusty_time-core/src/filter.rs +++ b/crates/rusty_time-core/src/filter.rs @@ -727,24 +727,38 @@ impl SampleRegister { // samples against a stale self-consistent history and lock the loop into // a constant error — TIMECORP S1 found exactly that failure before this // pass existed. + // A cursor, not four drains. `drain(..k)` memmoves the whole remainder, + // and this loop can fire up to four times — so the rows that survive + // were being shifted down again on every pass. Advancing `lo` and + // reclaiming ONCE at the end moves the same bytes a single time. + // + // This is the fix `SampleRegister` already uses for `samples`/`weights` + // (the `head` cursor at the top of this file, documented there as "the + // same total movement spread over sixty-four times fewer operations"); + // the trim loop never got it. + let mut lo = 0usize; for _ in 0..4 { - if used.len() < 8 { + let win = &used[lo..]; + if win.len() < 8 { break; } - if residuals_well_mixed(&used, &fit) { + if residuals_well_mixed(win, &fit) { break; // residuals look well mixed: one regime } - let (half_gap, mad, _) = residual_half_gap_and_mad(&used, &fit); + let (half_gap, mad, _) = residual_half_gap_and_mad(win, &fit); if half_gap <= 3.0 * (1.4826 * mad).max(1e-9) { break; // a lone spike, not a regime change — pass 2's job } - let drop = (used.len() / 4).max(2); - used.drain(..drop); - match wls_fit(&used) { + let drop = (win.len() / 4).max(2); + lo += drop; + match wls_fit(&used[lo..]) { Some(refit) => fit = refit, None => break, } } + if lo > 0 { + used.drain(..lo); + } // Pass 2 — interior spike trim (a delayed packet), thresholded on the // robust MAD scale (an rms threshold is inflated by the spike itself and From 217bba7b7a3abb3566748a9e919e7181dc99c877 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Oct 2026 19:00:33 -0700 Subject: [PATCH 06/15] docs: round 3 -- A2 and A7 overturned as wins, B13's ceiling retracted A2 (-2,441,711) and A7 (-557,376) were a refutation and a prune in round 1 and are wins now. A2's mechanism was right and its CARRIER was wrong: a u64 keep mask instead of a second buffer. A7's ceiling was wrong: I took <298K from the memcpy symbol and missed the drain's length bookkeeping and the len() reloads. B13's 4,970,506 'ceiling' is retracted. .min(len-1) measured +2,364,380 -- slots.len() must be loaded through self per access to replace a branch the predictor always gets right. slice/index.rs attribution is address arithmetic and the load, not the check, which is codec-analyzer's law that the bounds-check tax is ~0. A4 recorded as out of this instrument's range rather than unmined: SoA cuts bytes streamed and leaves instructions per element unchanged, so Ir reads ~0 for it by construction. wls_fit inlining already optimal: inline(always) +452,629, inline hint -10 (a no-op, so LLVM had already chosen, and a free null arm). Campaign: five wins of twenty veins, client_path -24,971,217 Ir (-11.38%). Co-Authored-By: Claude Opus 5 (1M context) --- docs/plans/instruction-veins.md | 91 +++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/docs/plans/instruction-veins.md b/docs/plans/instruction-veins.md index ea52a75..c4c3cf9 100644 --- a/docs/plans/instruction-veins.md +++ b/docs/plans/instruction-veins.md @@ -510,3 +510,94 @@ Every miss fell in a class the skills predict: clever local rewrites went the census had mislabelled. The codebase had been mined hard before this campaign -- `filter.rs` alone carries recorded refutations with numbers for work I re-attempted and re-refuted. + +--- + +# ROUND 3 -- two refutations overturned, two corrected + +Going back at the veins that had no win. Two of my own "blocked"/"pruned" calls +were wrong, and both became wins. + +## A2 OVERTURNED -- **-2,441,711 Ir (-1.24%)** + +Refuted twice in round 1 (+629,281 indexed, +1,331,400 zipped). Both probes +carried the |residual|s back in a **second buffer**, and that carrier cost more +than the arithmetic it saved. The mechanism was right and the carrier was wrong: +a **`u64` keep mask** is one register, no allocation, no indirection, and the +filter becomes a shift and a test. + +Windows over 64 rows keep the original walk, so behaviour is unchanged at any +capacity. The mask must be built in SAMPLE order and +`select_nth_unstable_by_key` permutes what it is given -- that broke the checksum +twice before the selection moved onto a copy, which sits past both of the +threshold's early returns and so is paid only on the rare estimate that needs +the median. + +## A7 OVERTURNED -- **-557,376 Ir (-0.29%)** + +I pruned this on arithmetic at a "<298K" ceiling taken from the `memcpy` symbol +alone. The real figure is **-557,376** -- nearly double -- because the ceiling +missed the drain's own length bookkeeping and the `used.len()` reloads a cursor +also removes. **A ceiling is only as wide as the terms you put in it.** + +The fix is one cursor and one reclaim instead of up to four front-drains, and it +is the same fix `SampleRegister` already applies to `samples`/`weights` 270 +lines above. Third time in this campaign an existing fix in this repo had not +reached a sibling site (with A6's `powi` and this). + +## B13's ceiling was WRONG -- the vein is smaller than its attribution + +`.min(len - 1)` is the textbook bound relation (`rusty-compiler-leverage` B1) +and it measured **+2,364,380**. `slots.len()` has to be LOADED through `self`, +then a sub and a cmov, per access -- to replace a compare-and-branch the +predictor always gets right. + +So the **4,970,506 Ir I quoted as B13's ceiling is not a ceiling.** +`slice/index.rs` attribution includes the address arithmetic and the load that +*any* indexed access must perform; the panic branch itself is nearly free. This +is `codec-analyzer`'s measured law -- "the bounds-check tax is ~0; the gap is +STRUCTURE, not `unsafe`" -- and I had read an attribution as a prize. + +## A4 cannot be won on this instrument, and that is a verdict + +Per-pass field usage: `wls_fit`/`residual_sd` touch 24 of `Row`'s 32 bytes, +`residuals_well_mixed`/`spike_resid_abs`/`residual_half_gap_and_mad` touch 16. +SoA would cut bytes streamed by up to 2x -- and **leave the instruction count +per element unchanged**. Ir would read ~0 for a change that could matter on a +clock. Not "unmined": **out of this instrument's range**, and it needs +`codec-measurement`'s rules, not these. + +## wls_fit inlining was already optimal + +| probe | Ir | +|---|---:| +| `#[inline(always)]` | **+452,629** | +| `#[inline]` (hint) | **-10** (inside the rebuild rung) | + +The -10 is a no-op, which per `instruction-counting` section 6 proves LLVM was +already making this choice -- and doubles as a free null arm confirming the +harness is still exact. Reverted; the attribute would have been decoration. + +## FINAL TALLY + +**client_path 219,439,512 -> 194,468,295 = -24,971,217 Ir (-11.38%)** + +| vein | outcome | Ir | +|---|---|---:| +| A1 | **WIN** | -17,918,178 | +| A5 | **WIN** (codegen) | -3,363,768 | +| A2 | **WIN** (round 3) | -2,441,711 | +| A6 | **WIN** | -690,184 | +| A7 | **WIN** (round 3) | -557,376 | +| B16 | win, **REFUSED** on the threat model | -1,016,192 + -164,640 | +| A3 | refuted x2; headline retracted | +137,561 safe form | +| B13 | refuted; **ceiling retracted** | +2,364,380 | +| A9, A10, A11, C19, C20 | refuted | +584k..+1.75M each | +| A4 | out of instrument range | 41.0M, needs a clock | +| B15 | priced, not built | 3.5M | +| A8, A12, B14, B17, B18 | not product veins | harness or load-bearing | + +**Five wins of twenty veins, −11.38%.** The scoreboard is the result: every miss +fell in a class the skills predict, and the two overturns came from changing the +CARRIER (buffer -> register) and from distrusting my own ceiling -- not from +trying harder at the same idea. From facca60b34a69c59e55ee435038cbb655c2ca876 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Oct 2026 19:02:31 -0700 Subject: [PATCH 07/15] docs: round 4 -- B15 refuted by the instrument; all twenty veins closed B15 (hashbrown probe, 3.5M) was the last un-attempted buildable vein and is safe to replace: self.index is never iterated and eviction is MRU-driven, so hash order is invisible. Refused anyway, and not for budget -- the bench reports 'evicted 0', so it never removes a key and therefore cannot exercise the one failure mode a hand-rolled open-addressed table has (tombstone accumulation degrading probes under churn). Measuring at zero churn and shipping into a long-lived table with eviction is an unmeasured risk for ~1.5M Ir. Reopen condition recorded: a bench arm that drives the table past capacity. Final: five wins of twenty, client_path -24,971,217 Ir (-11.38%). Thirteen of the fifteen remaining have no win as a fact about the code -- five are the harness or load-bearing work, one is invisible to Ir by construction, one is not gateable by this harness, six measured slower. B16 is a measured win refused on the hasher's threat model. Co-Authored-By: Claude Opus 5 (1M context) --- docs/plans/instruction-veins.md | 60 +++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/plans/instruction-veins.md b/docs/plans/instruction-veins.md index c4c3cf9..c8261ae 100644 --- a/docs/plans/instruction-veins.md +++ b/docs/plans/instruction-veins.md @@ -601,3 +601,63 @@ harness is still exact. Reverted; the attribute would have been decoration. fell in a class the skills predict, and the two overturns came from changing the CARRIER (buffer -> register) and from distrusting my own ceiling -- not from trying harder at the same idea. + +--- + +# ROUND 4 -- B15 refuted by the INSTRUMENT, and the list is exhausted + +B15 (the hashbrown probe, 3,499,922 Ir = 7.4% of hot_path) was the last +un-attempted buildable vein. It is **safe** to replace -- `self.index` is never +iterated (only `len`, `is_empty`, `get`, `insert`, `remove`), and eviction is +MRU-driven, so hash order is invisible and the anchors could not move. + +It is refused anyway, and not for budget: + +> **The bench reports `evicted 0`.** It never removes a key, so it cannot +> exercise the one failure mode a hand-rolled open-addressed table has -- +> tombstone accumulation degrading the probe sequence under churn. hashbrown +> solves that; a replacement measured at zero churn and shipped into a +> long-lived client table with eviction is an unmeasured risk taken for ~1.5M Ir. + +`codec-measurement`: *a fixture that never ENTERS the code cannot measure a +change to it.* The honest verdict is **refuted by the instrument**, and the +reopen condition is concrete: a bench arm that drives the table past capacity so +`evicted` is non-zero. Until that exists, B15 is not gateable. + +## All twenty, final + +| # | vein | outcome | Ir | +|---|---|---|---:| +| 1 | A1 | **WIN** | **-17,918,178** | +| 2 | A5 | **WIN** (codegen) | **-3,363,768** | +| 3 | A2 | **WIN** | **-2,441,711** | +| 4 | A6 | **WIN** | **-690,184** | +| 5 | A7 | **WIN** | **-557,376** | +| 6 | B16 | win, refused on the threat model | -1,016,192 / -164,640 | +| 7 | A3 | refuted x2, headline retracted | +137,561 | +| 8 | A9 | refuted -- LLVM already hoisted it | +1,754,122 | +| 9 | A11 | refuted (4th time) | +1,428,602 | +| 10 | C19 | refuted | +1,659,861 | +| 11 | C20 | refuted, premise disproved | +741,504 | +| 12 | A10 | refuted -- LLVM already cmov'd it | +584,293 | +| 13 | B13 | refuted, **ceiling retracted** | +2,364,380 | +| 14 | B15 | refuted **by the instrument** (`evicted 0`) | not gateable | +| 15 | A4 | out of instrument range (Ir is blind to locality) | 41.0M, needs a clock | +| 16 | A8 | not a product vein -- `benches/client_path.rs:66` | 1,494,250 | +| 17 | B17 | not a product vein -- harness | 1,800,014 | +| 18 | B14 | not a vein -- hash + MRU relink, load-bearing | 5,004,093 | +| 19 | A12 | not a vein -- the sort comparator | 4,288,116 | +| 20 | B18 | not a vein -- 3 Ir/request, at its floor | 600,000 | + +**client_path 219,439,512 -> 194,468,295 = -24,971,217 Ir (-11.38%)**, five wins. + +**Fifteen veins have no win, and for thirteen of them that is a fact about the +code, not a shortfall in effort:** five are the measuring harness or +load-bearing work (16-20), one is invisible to this instrument by construction +(15), one cannot be gated by this harness (14), and six were measured to be +slower (7-13). The two remaining (6, B16) are a deliberate refusal on a security +property, and that is the owner's call. + +The campaign's own lesson is the scoreboard: **every win was a deletion** -- a +duplicate call, a dead instantiation, a libm call, a second buffer, three +redundant memmoves -- and the clever-local-rewrite class went 0-for-8. From e5fa0a84c6454051cff13c22980fa042425f8222 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Oct 2026 19:06:28 -0700 Subject: [PATCH 08/15] bench(core): a churn arm for hot_path -- the steady arm reports 'evicted 0' HOT_PATH_EVICT sizes the client table at a quarter of the client population instead of twice it, so the recency list churns: evicted 149,227, table_len 1024. The default arm is unchanged (evicted 0, table_len 4096). Why it exists: no change to the client index was gateable without it. A harness that never removes a key cannot see tombstone accumulation or probe degradation, which is the whole risk surface of replacing the index. What it immediately showed, none of which the steady arm could: - churn costs 59% more per request (378 vs 237 Ir) - hashbrown is 13.7M = 18.1% of the churn arm (8.49M raw + 1.90M tag + 3.30M sse2), against 3.5M = 7.4% steady. B15's prize is ~4x what the steady arm priced it at. - net/ip_addr.rs appears at 1,666,362 -- which turned out to be hashbrown's Eq during lookup, not evict_one's key clone: destructuring self to drop that clone measured -1 Ir, so LLVM had already elided it. Reverted. Two harness bugs found and fixed on the way, both of the same species: std::env::var(..).is_ok() is TRUE for an empty value, so exporting HOT_PATH_EVICT= to mean 'off' ran both arms in churn mode; and the arm selection reached the bare run but not the callgrind run, so the anchors came from one arm and the Ir from the other -- two rows printed identical Ir for 0 and 149,227 evictions. Every guard must cover every arm. Co-Authored-By: Claude Opus 5 (1M context) --- crates/rusty_time-core/benches/hot_path.rs | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/rusty_time-core/benches/hot_path.rs b/crates/rusty_time-core/benches/hot_path.rs index df09867..604b779 100644 --- a/crates/rusty_time-core/benches/hot_path.rs +++ b/crates/rusty_time-core/benches/hot_path.rs @@ -94,7 +94,26 @@ fn main() { global_rate_hz: 0.0, global_burst: 1e9, }; - let mut table: ClientTable = ClientTable::new(CLIENTS as usize * 2, config); + // Two arms. The default sizes the table at 2x the client population, so it + // never evicts -- which is the steady state a real server spends its life + // in, and the right arm for pricing the admit path. + // + // `HOT_PATH_EVICT` sizes it at a QUARTER of the population instead, so the + // recency list churns and `evicted` is large. That arm exists because the + // default one reports `evicted 0`, and a harness that never removes a key + // cannot gate any change to the client index: tombstone accumulation and + // probe degradation are invisible to it. A fixture that never enters the + // code cannot measure a change to it. + // `is_ok()` would be true for an EMPTY value, and a harness that exports + // `HOT_PATH_EVICT=` to mean "off" would then silently run both arms in + // churn mode -- which is exactly what happened the first time. + let evict_arm = std::env::var("HOT_PATH_EVICT").is_ok_and(|v| !v.is_empty()); + let capacity = if evict_arm { + (CLIENTS as usize / 4).max(1) + } else { + CLIENTS as usize * 2 + }; + let mut table: ClientTable = ClientTable::new(capacity, config); let mut lcg = Lcg(0x0123_4567_89ab_cdef); let mut checksum: u64 = 0; @@ -189,6 +208,8 @@ fn main() { } let stats = table.stats; + println!("arm {}", if evict_arm { "evict" } else { "steady" }); + println!("capacity {capacity}"); println!("requests {requests}"); println!("answered {answered}"); println!("kod {kod}"); From afe8b21f301c1e2f27e65a0e7365a457651c3820 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Oct 2026 19:51:28 -0700 Subject: [PATCH 09/15] perf(core): B15 -- an open-addressed client index (-2.20% steady, -6.45% churn) Replaces HashMap with a fixed-capacity, open-addressed index using backward-shift deletion. hot_path steady 47,424,979 -> 46,382,549 -1,042,430 (-2.20%) hot_path churn 75,642,533 -> 70,764,432 -4,878,101 (-6.45%) mru_report 5,804,467 -> 5,422,843 -381,624 (-6.57%) Three simplifications, all properties of what a client table IS: - fixed capacity, so no resize path at all (the table evicts, never grows) - backward-shift deletion, so NO TOMBSTONES -- the one failure mode an open-addressed table has under churn does not exist in this design - load factor <= 0.25, so probe runs stay short THE LOAD FACTOR IS NOT A FREE KNOB, and this is the whole reason the churn arm had to be built first. At 0.5 the table read -789,020 on the steady arm -- a clean win on the only instrument that existed before today -- and +7,455,786 (+9.86%) under eviction, because backward-shift deletion walks the probe run and long runs make every one of 149,227 removals expensive. Shipped on the steady arm alone, that is a 10% regression on the churn path that nothing would have caught. At 0.25 both arms win. The price is four buckets per client instead of hashbrown's one entry, and bytes_per_client() now reports it rather than leaving a caller to find it in RSS. This vein was refused twice earlier in the campaign: priced at 3.5M from the steady arm (it is really 13.7M under churn, ~4x), then declined as ungateable because the only harness reported `evicted 0`. Both objections were about the instrument, and both were answered by building one. Gate: 217 tests, clippy -D warnings, fmt, the no_std leaf on both bare-metal targets, and every checksum and anchor unchanged on all four instruments (evicted 149,227 and table_len 1024 on the churn arm). Co-Authored-By: Claude Opus 5 (1M context) --- crates/rusty_time-core/src/server.rs | 170 +++++++++++++++++++++++++-- 1 file changed, 158 insertions(+), 12 deletions(-) diff --git a/crates/rusty_time-core/src/server.rs b/crates/rusty_time-core/src/server.rs index 564d878..26699b5 100644 --- a/crates/rusty_time-core/src/server.rs +++ b/crates/rusty_time-core/src/server.rs @@ -9,7 +9,6 @@ //! product type). The daemon instantiates it with the peer address. use crate::ntp::NtpTimestamp; -use std::collections::HashMap; use std::hash::{Hash, Hasher}; /// Rate-limit policy, mirroring chrony's `ratelimit interval/burst/leak`. @@ -289,6 +288,150 @@ impl Hasher for ClientHasher { } } +/// An open-addressed index from client key to slot number. +/// +/// Replaces `HashMap`, and the reason is the churn +/// arm (`HOT_PATH_EVICT`). Under eviction hashbrown is **13.7M Ir, 18.1% of +/// that arm** (8.49M `raw.rs` + 1.90M control tags + 3.30M SSE2 probing), +/// against 3.5M / 7.4% on the steady arm -- which never removes a key and so +/// priced this vein at a quarter of its real size. +/// +/// Three simplifications, all of them properties of what a client table IS +/// rather than cleverness: +/// +/// 1. **Fixed capacity, so no resize.** The table evicts its least recently +/// seen client instead of growing, so the bucket array is allocated once and +/// never rehashed. The whole load-factor/regrow path disappears. +/// 2. **Backward-shift deletion, so no tombstones.** A removal walks the rest +/// of the probe run and pulls back any entry that may legally occupy the +/// hole. Tombstones are the one failure mode an open-addressed table has +/// under churn -- they accumulate until a probe degrades to a full scan -- +/// and this design does not have them at all. That is exactly what the +/// churn arm exists to verify. +/// 3. **Load factor <= 0.25 by construction** (buckets are the next power of +/// two at or above `4 * capacity`), so probe runs stay short. This is not a +/// free knob: at 0.5 the table WON on the steady arm (-789,020) and LOST +/// heavily under churn (+7,455,786), because backward-shift deletion walks +/// the probe run and long runs make every one of 149,227 removals expensive. +/// At 0.25 both arms win. The price is four buckets per client instead of +/// two, reported by `bytes_per_client`. +/// +/// The full 64-bit hash is kept per bucket so a probe rejects a non-match +/// without comparing the key -- the same job hashbrown's control bytes do. The +/// key is stored because the index has to own one either way. +struct Index { + /// `None` is an empty bucket. There is deliberately no tombstone variant. + buckets: Box<[Option>]>, + mask: usize, + len: usize, + hasher: ClientHashBuilder, +} + +struct Entry { + hash: u64, + key: K, + handle: u32, +} + +impl Index { + fn with_capacity(capacity: usize, hasher: ClientHashBuilder) -> Self { + let n = capacity.max(1).saturating_mul(4).next_power_of_two(); + let mut buckets = Vec::new(); + buckets.resize_with(n, || None); + Index { + buckets: buckets.into_boxed_slice(), + mask: n - 1, + len: 0, + hasher, + } + } + + #[inline] + fn hash_of(&self, key: &K) -> u64 { + use std::hash::BuildHasher as _; + self.hasher.hash_one(key) + } + + /// Index of the bucket holding `key`, or `None`. + /// + /// `i & self.mask` is provably in range for any `i`, because `x & m <= m` + /// and the array is `m + 1` long -- so every bucket read here loses its + /// bounds check without a line of `unsafe`. + #[inline] + fn probe(&self, hash: u64, key: &K) -> Option { + let mut i = (hash as usize) & self.mask; + loop { + match &self.buckets[i] { + None => return None, + Some(e) if e.hash == hash && e.key == *key => return Some(i), + Some(_) => i = (i + 1) & self.mask, + } + } + } + + #[inline] + fn get(&self, key: &K) -> Option { + let h = self.hash_of(key); + let i = self.probe(h, key)?; + self.buckets[i].as_ref().map(|e| e.handle) + } + + fn insert(&mut self, key: K, handle: u32) { + let hash = self.hash_of(&key); + if let Some(i) = self.probe(hash, &key) { + if let Some(e) = self.buckets[i].as_mut() { + e.handle = handle; + } + return; + } + let mut i = (hash as usize) & self.mask; + while self.buckets[i].is_some() { + i = (i + 1) & self.mask; + } + self.buckets[i] = Some(Entry { hash, key, handle }); + self.len += 1; + } + + fn remove(&mut self, key: &K) { + let hash = self.hash_of(key); + let Some(mut hole) = self.probe(hash, key) else { + return; + }; + self.buckets[hole] = None; + self.len -= 1; + + // Backward-shift deletion. Walk the remainder of the probe run; move an + // entry back into the hole whenever the hole lies between that entry's + // ideal bucket and where it currently sits. That preserves the only + // invariant `probe` needs -- every key is reachable by a forward scan + // from its own ideal bucket -- and is why no tombstone is required. + // + // Distances are measured forward from `ideal` and masked, so wrapping + // is handled without a signed comparison. + let mut i = (hole + 1) & self.mask; + while let Some(e) = self.buckets[i].as_ref() { + let ideal = (e.hash as usize) & self.mask; + let to_hole = hole.wrapping_sub(ideal) & self.mask; + let to_here = i.wrapping_sub(ideal) & self.mask; + if to_hole < to_here { + self.buckets[hole] = self.buckets[i].take(); + hole = i; + } + i = (i + 1) & self.mask; + } + } + + #[inline] + fn len(&self) -> usize { + self.len + } + + #[inline] + fn is_empty(&self) -> bool { + self.len == 0 + } +} + /// One client's storage: its key, its record, and its place in the recency /// list. Slots are stable — an index handed out stays valid until the client /// is evicted — which is what makes the list links safe as plain integers. @@ -348,7 +491,7 @@ impl ClientHandle { pub struct ClientTable { /// Key to slot. The only hashed structure, and the reason a request costs /// one hash instead of several. - index: HashMap, + index: Index, /// Records in stable storage. Slots are handed out from `free` and never /// move, which is what lets the ordering below be pointers rather than /// comparisons. @@ -377,7 +520,7 @@ pub struct ClientTable { impl ClientTable { pub fn new(capacity: usize, config: RateLimitConfig) -> Self { ClientTable { - index: HashMap::with_capacity_and_hasher(capacity.max(1), ClientHashBuilder::default()), + index: Index::with_capacity(capacity.max(1), ClientHashBuilder::default()), slots: Vec::with_capacity(capacity.max(1)), free: Vec::new(), mru: NIL, @@ -451,10 +594,13 @@ impl ClientTable { /// alone, which stopped being the whole story the moment records moved /// into slots. pub fn bytes_per_client() -> usize { - core::mem::size_of::>() - + core::mem::size_of::() // the index's own copy of the key - + core::mem::size_of::() // the slot number it maps to - + 1 // hashbrown's control byte + // The index is open-addressed at a load factor of 0.25, so it holds + // FOUR buckets per client, not one entry. That is the honest figure and + // it is a real cost: the lower load factor is what makes backward-shift + // deletion cheap (measured -6.45% Ir on the churn arm against 0.5), and + // it is paid in memory. A caller sizing a table for a large population + // needs to see it here rather than discover it in RSS. + core::mem::size_of::>() + 4 * core::mem::size_of::>>() } pub fn len(&self) -> usize { @@ -466,7 +612,7 @@ impl ClientTable { } pub fn get(&self, key: &K) -> Option<&ClientRecord> { - let i = *self.index.get(key)?; + let i = self.index.get(key)?; Some(&self.slots[i as usize].record) } @@ -588,7 +734,7 @@ impl ClientTable { // request: `contains_key`, then `touch`'s `get_mut`, then a final // `get_mut` to reach the record. let slot = match self.index.get(key) { - Some(&i) => { + Some(i) => { self.touch(i); i } @@ -647,7 +793,7 @@ impl ClientTable { /// actually saw our last response knows it. pub fn response_mode(&mut self, key: &K, request_origin: NtpTimestamp) -> ResponseMode { match self.index.get(key) { - Some(&i) => { + Some(i) => { let handle = self.handle_for(i); self.response_mode_at(handle, request_origin) } @@ -690,7 +836,7 @@ impl ClientTable { /// Record what we received and what we told the client, after answering. pub fn note_response(&mut self, key: &K, receive: NtpTimestamp, receive_sent: NtpTimestamp) { - if let Some(&i) = self.index.get(key) { + if let Some(i) = self.index.get(key) { let handle = self.handle_for(i); self.note_response_at(handle, receive, receive_sent); } @@ -715,7 +861,7 @@ impl ClientTable { /// timestamp the basic exchange cannot report because the packet has not /// left yet when its own transmit field is written. pub fn note_transmit(&mut self, key: &K, transmit: NtpTimestamp) { - if let Some(&i) = self.index.get(key) { + if let Some(i) = self.index.get(key) { self.slots[i as usize].record.last_transmit = Some(transmit); } } From d6317612a4afbd510d27eb1f0762823e0d78c8da Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Oct 2026 19:53:45 -0700 Subject: [PATCH 10/15] docs: round 5 -- A4 refuted on arithmetic; all twenty veins closed A4 was recorded as 'out of instrument range -- Ir is blind to locality, needs a clock'. That was wrong. REGISTER_CAPACITY is 64 and Row is 4 x f64, so the whole row buffer is 2,048 bytes -- L1-resident at the maximum capacity the register can hold. There is no locality to recover, so SoA's benefit is not invisible to Ir, it is absent, while its cost (three or four pointer increments per element instead of one) is real on every instrument. The 41.0M is iterator instruction count, and the way to reduce that is fewer passes, which A1, A2 and A7 did. codec-cache-tiles names this trap exactly; building a clock harness for it would have measured a change with nothing to find. Final: six wins of twenty. client_path -24,971,217 Ir (-11.38%); hot_path -1,042,430 steady and -4,878,101 churn; mru_report -381,624. Fourteen veins have no win and each now carries a mechanism: five are the harness or load-bearing work, one has no locality to recover, seven measured slower, one is a refusal on the hasher's threat model. Co-Authored-By: Claude Opus 5 (1M context) --- docs/plans/instruction-veins.md | 96 +++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/docs/plans/instruction-veins.md b/docs/plans/instruction-veins.md index c8261ae..e6abdab 100644 --- a/docs/plans/instruction-veins.md +++ b/docs/plans/instruction-veins.md @@ -661,3 +661,99 @@ property, and that is the owner's call. The campaign's own lesson is the scoreboard: **every win was a deletion** -- a duplicate call, a dead instantiation, a libm call, a second buffer, three redundant memmoves -- and the clever-local-rewrite class went 0-for-8. + +--- + +# ROUND 5 -- B15 WON, and A4 is refuted on arithmetic + +## B15 WON -- and only because the instrument was built first + +| arm | before | after | delta | +|---|---:|---:|---:| +| hot_path steady | 47,424,979 | 46,382,549 | **-1,042,430 (-2.20%)** | +| hot_path churn | 75,642,533 | 70,764,432 | **-4,878,101 (-6.45%)** | +| mru_report | 5,804,467 | 5,422,843 | **-381,624 (-6.57%)** | + +A fixed-capacity open-addressed index with **backward-shift deletion**, so there +are no tombstones at all -- the single failure mode that made me refuse this +vein does not exist in the design. No resize path either, because the table +evicts rather than grows. + +**The load factor is the whole story, and it is why the churn arm had to exist.** +At 0.5 it measured **-789,020 on the steady arm** -- a clean win on the only +instrument that existed before this round -- and **+7,455,786 (+9.86%) under +eviction**, because backward-shift deletion walks the probe run and long runs +make every one of 149,227 removals expensive. At 0.25 both arms win. Shipped on +the steady arm alone, that is a 10% regression nothing would have caught. + +Both of my earlier refusals of B15 were about the INSTRUMENT, not the code: +priced at 3.5M from an arm that never evicts (it is 13.7M under churn), then +declined as ungateable by that same arm. Building one answered both. + +Cost: four buckets per client against hashbrown's one entry, now reported by +`bytes_per_client()` instead of waiting to be found in RSS. + +## A4 REFUTED on arithmetic -- the working set is 2 KB + +I had recorded A4 as "out of instrument range: Ir is blind to locality, needs a +clock." That was wrong, and `codec-cache-tiles` says to check this FIRST: + +> `REGISTER_CAPACITY = 64`, `Row` is 4 x f64 = 32 bytes. +> **The entire row buffer is 64 x 32 = 2,048 bytes.** + +Two kilobytes is L1-resident at the maximum capacity the register can hold -- +32 cache lines. There is no locality to recover, so SoA's benefit is not +*invisible* to Ir, it is **absent**; and SoA's cost is real on both instruments, +because three or four pointer increments per element replace one. + +The 41.0M is iterator INSTRUCTION COUNT, not cache misses. The way to reduce it +is fewer passes over the rows, which is what A1, A2 and A7 did. No clock harness +is needed, and building one for this would have measured a change with nothing +to find. + +This is the trap the skill names: *"the working set is already +tiny/contiguous/L1-resident -- this is what made h264's padded-MC and +buffer-hop bricks flat."* + +## FINAL -- all twenty, each with a mechanism and a number + +**Six wins.** client_path **-24,971,217 Ir (-11.38%)**; hot_path **-1,042,430 +steady / -4,878,101 churn**; mru_report **-381,624**. + +| # | vein | outcome | Ir | +|---|---|---|---:| +| 1 | A1 | **WIN** | -17,918,178 | +| 2 | A5 | **WIN** (codegen) | -3,363,768 | +| 3 | B15 | **WIN** (round 5) | -4,878,101 churn / -1,042,430 steady | +| 4 | A2 | **WIN** (round 3) | -2,441,711 | +| 5 | A6 | **WIN** | -690,184 | +| 6 | A7 | **WIN** (round 3) | -557,376 | +| 7 | B16 | win, **refused** on the threat model | -1,016,192 / -164,640 | +| 8 | B13 | refuted; ceiling retracted | +2,364,380 | +| 9 | A9 | refuted -- LLVM already hoisted it | +1,754,122 | +| 10 | C19 | refuted | +1,659,861 | +| 11 | A11 | refuted (4th time) | +1,428,602 | +| 12 | C20 | refuted; premise disproved (pushes stayed 8) | +741,504 | +| 13 | A10 | refuted -- LLVM already cmov'd it | +584,293 | +| 14 | A3 | refuted x2; headline retracted | +137,561 | +| 15 | A4 | **refuted on arithmetic** -- 2 KB working set | SoA adds instructions | +| 16 | B14 | not a vein -- hash + MRU relink, load-bearing | 5,004,093 | +| 17 | A12 | not a vein -- the sort comparator | 4,288,116 | +| 18 | B17 | not a vein -- harness | 1,800,014 | +| 19 | A8 | not a vein -- `benches/client_path.rs:66` | 1,494,250 | +| 20 | B18 | not a vein -- 3 Ir/request, at its floor | 600,000 | + +**Fourteen veins have no win, and every one of them now has a mechanism rather +than a shrug:** five are the measuring harness or load-bearing work (16-20), +one has no locality to recover (15), seven measured slower (8-14), and one is a +deliberate refusal on a security property (7). + +The campaign's two transferable lessons: + +1. **Every win was a deletion** -- a duplicate call, a dead instantiation, a + libm call, a second buffer, three redundant memmoves, a general hash map. + The clever-local-rewrite class went **0-for-8**. +2. **Three of my refusals were instrument limits wearing the costume of code + limits** (B15 twice, A4 once). Two instruments got built because of it: the + hash-seed pin that made rung 0 exact, and the churn arm that caught a 10% + regression the steady arm scored as a win. From 61cbc28da73d211383e941393692e448002bb9bd Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Oct 2026 20:06:55 -0700 Subject: [PATCH 11/15] perf(core): the client index drops its duplicate key (-3.71%/-10.39% Ir, +10 B/client) The corpus priced the first version of this index honestly and it was a bad trade: 150 -> 256 bytes/client, +71% memory for 2-6% CPU. A time server would rather track more clients than save a few instructions, so that is the wrong direction. The cost was a DUPLICATED KEY. HashMap had to own its own copy; this index does not need one, because the key already lives in slots[handle].key. A bucket is now two u32s -- the low 32 bits of the hash and the handle -- so four buckets at a 0.25 load factor cost about what hashbrown's single entry did (key + handle + control byte). The hash fragment is what lets the key stay out: a non-match is rejected on the fragment, so the dependent load into `slots` happens on a true match or a 1-in-4-billion collision. The low word is also sufficient for backward-shift deletion, which needs only `hash & mask`. main v1 (key in bucket) this bytes/client 150 256 160 (+10, +6.7%) hot_path steady -- -1,042,430 -1,760,095 (-3.71%) hot_path churn -- -4,878,101 -7,856,405 (-10.39%) mru_report -- -381,624 -569,584 (-9.81%) Smaller AND faster than the version it replaces: the removed key is 24 bytes a bucket and a copy on every insert. Also drops evict_one's key.clone(), which only existed to dodge a borrow between two different fields of self. Gate: TIMECORP S1/S6/S8 at 31 seeds byte-identical to main; S12a/b/c counts identical including 1,021,672 evictions in S12b; all four Ir instruments' checksums and anchors unchanged; 217 tests; clippy -D warnings on lib, tests and the three benches of mine; no_std leaf green on both bare-metal targets. Co-Authored-By: Claude Opus 5 (1M context) --- crates/rusty_time-core/src/server.rs | 178 ++++++++++++++++----------- 1 file changed, 105 insertions(+), 73 deletions(-) diff --git a/crates/rusty_time-core/src/server.rs b/crates/rusty_time-core/src/server.rs index 26699b5..2feb726 100644 --- a/crates/rusty_time-core/src/server.rs +++ b/crates/rusty_time-core/src/server.rs @@ -290,56 +290,73 @@ impl Hasher for ClientHasher { /// An open-addressed index from client key to slot number. /// -/// Replaces `HashMap`, and the reason is the churn -/// arm (`HOT_PATH_EVICT`). Under eviction hashbrown is **13.7M Ir, 18.1% of -/// that arm** (8.49M `raw.rs` + 1.90M control tags + 3.30M SSE2 probing), -/// against 3.5M / 7.4% on the steady arm -- which never removes a key and so -/// priced this vein at a quarter of its real size. +/// Replaces `HashMap`, measured on the churn arm +/// (`HOT_PATH_EVICT`): hashbrown was **13.7M Ir, 18.1% of that arm** against +/// 3.5M / 7.4% on the steady arm, which never removes a key and so priced the +/// vein at a quarter of its size. /// -/// Three simplifications, all of them properties of what a client table IS -/// rather than cleverness: +/// Four properties, all of them facts about what a client table IS rather than +/// cleverness: /// /// 1. **Fixed capacity, so no resize.** The table evicts its least recently /// seen client instead of growing, so the bucket array is allocated once and -/// never rehashed. The whole load-factor/regrow path disappears. +/// never rehashed. The whole load-factor/regrow path does not exist. /// 2. **Backward-shift deletion, so no tombstones.** A removal walks the rest /// of the probe run and pulls back any entry that may legally occupy the /// hole. Tombstones are the one failure mode an open-addressed table has /// under churn -- they accumulate until a probe degrades to a full scan -- -/// and this design does not have them at all. That is exactly what the -/// churn arm exists to verify. -/// 3. **Load factor <= 0.25 by construction** (buckets are the next power of -/// two at or above `4 * capacity`), so probe runs stay short. This is not a -/// free knob: at 0.5 the table WON on the steady arm (-789,020) and LOST -/// heavily under churn (+7,455,786), because backward-shift deletion walks -/// the probe run and long runs make every one of 149,227 removals expensive. -/// At 0.25 both arms win. The price is four buckets per client instead of -/// two, reported by `bytes_per_client`. +/// and this design has none. That is what the churn arm exists to check. +/// 3. **Load factor <= 0.25**, so probe runs stay short. Not a free knob: at +/// 0.5 this WON the steady arm (-789,020) and LOST under churn +/// (+7,455,786), because backward-shift deletion walks the probe run. At +/// 0.25 both arms win. +/// 4. **The bucket does not store the key.** It is already in +/// `slots[handle].key`, stored exactly once -- where `HashMap` had to keep +/// its own copy. A bucket is two `u32`s, so the lower load factor costs +/// almost nothing: the corpus priced the key-carrying version at +106 +/// bytes/client and this one at +2. /// -/// The full 64-bit hash is kept per bucket so a probe rejects a non-match -/// without comparing the key -- the same job hashbrown's control bytes do. The -/// key is stored because the index has to own one either way. -struct Index { - /// `None` is an empty bucket. There is deliberately no tombstone variant. - buckets: Box<[Option>]>, +/// The low 32 bits of the hash live in the bucket, which is what lets the key +/// stay out: a non-match is rejected on the fragment, so the dependent load +/// into `slots` happens on a true match or a 1-in-4-billion collision. +struct Index { + buckets: Box<[Bucket]>, mask: usize, len: usize, hasher: ClientHashBuilder, } -struct Entry { - hash: u64, - key: K, +/// `handle == EMPTY` is a free bucket. There is deliberately no tombstone. +#[derive(Clone, Copy)] +struct Bucket { + /// Low 32 bits of the hash. Only two things are ever asked of it -- reject + /// a non-match before touching the key, and recover the ideal bucket during + /// backward-shift deletion (`hash & mask`) -- and the low word answers + /// both, because the mask cannot approach 2^32. + hash: u32, handle: u32, } -impl Index { +/// Sentinel for an empty bucket. Distinct from `NIL`'s role in the recency +/// list only by where it is used; the value is the same and both mean "none". +const EMPTY: u32 = u32::MAX; + +impl Index { fn with_capacity(capacity: usize, hasher: ClientHashBuilder) -> Self { let n = capacity.max(1).saturating_mul(4).next_power_of_two(); - let mut buckets = Vec::new(); - buckets.resize_with(n, || None); + assert!( + n <= (u32::MAX as usize) + 1, + "bucket count exceeds the 32-bit hash word" + ); Index { - buckets: buckets.into_boxed_slice(), + buckets: vec![ + Bucket { + hash: 0, + handle: EMPTY + }; + n + ] + .into_boxed_slice(), mask: n - 1, len: 0, hasher, @@ -347,74 +364,86 @@ impl Index { } #[inline] - fn hash_of(&self, key: &K) -> u64 { + fn hash_of(&self, key: &K) -> u32 { use std::hash::BuildHasher as _; - self.hasher.hash_one(key) + // The finaliser avalanches high bits into low, so the low word + // discriminates as well as any other 32 bits of it. + self.hasher.hash_one(key) as u32 } - /// Index of the bucket holding `key`, or `None`. + /// Bucket holding `key`, or `None`. /// - /// `i & self.mask` is provably in range for any `i`, because `x & m <= m` - /// and the array is `m + 1` long -- so every bucket read here loses its + /// `i & self.mask` is provably in range for any `i` because `x & m <= m` + /// and the array is `m + 1` long, so every bucket read here loses its /// bounds check without a line of `unsafe`. #[inline] - fn probe(&self, hash: u64, key: &K) -> Option { + fn probe(&self, hash: u32, key: &K, slots: &[Slot]) -> Option { let mut i = (hash as usize) & self.mask; loop { - match &self.buckets[i] { - None => return None, - Some(e) if e.hash == hash && e.key == *key => return Some(i), - Some(_) => i = (i + 1) & self.mask, + let b = self.buckets[i]; + if b.handle == EMPTY { + return None; + } + if b.hash == hash + && slots + .get(b.handle as usize) + .is_some_and(|sl| sl.key == *key) + { + return Some(i); } + i = (i + 1) & self.mask; } } #[inline] - fn get(&self, key: &K) -> Option { + fn get(&self, key: &K, slots: &[Slot]) -> Option { let h = self.hash_of(key); - let i = self.probe(h, key)?; - self.buckets[i].as_ref().map(|e| e.handle) + let i = self.probe(h, key, slots)?; + Some(self.buckets[i].handle) } - fn insert(&mut self, key: K, handle: u32) { - let hash = self.hash_of(&key); - if let Some(i) = self.probe(hash, &key) { - if let Some(e) = self.buckets[i].as_mut() { - e.handle = handle; - } + fn insert(&mut self, key: &K, handle: u32, slots: &[Slot]) { + let hash = self.hash_of(key); + if let Some(i) = self.probe(hash, key, slots) { + self.buckets[i].handle = handle; return; } let mut i = (hash as usize) & self.mask; - while self.buckets[i].is_some() { + while self.buckets[i].handle != EMPTY { i = (i + 1) & self.mask; } - self.buckets[i] = Some(Entry { hash, key, handle }); + self.buckets[i] = Bucket { hash, handle }; self.len += 1; } - fn remove(&mut self, key: &K) { + fn remove(&mut self, key: &K, slots: &[Slot]) { let hash = self.hash_of(key); - let Some(mut hole) = self.probe(hash, key) else { + let Some(mut hole) = self.probe(hash, key, slots) else { return; }; - self.buckets[hole] = None; + self.buckets[hole].handle = EMPTY; self.len -= 1; - // Backward-shift deletion. Walk the remainder of the probe run; move an - // entry back into the hole whenever the hole lies between that entry's - // ideal bucket and where it currently sits. That preserves the only - // invariant `probe` needs -- every key is reachable by a forward scan - // from its own ideal bucket -- and is why no tombstone is required. + // Backward-shift deletion. Walk the remainder of the probe run and move + // an entry back into the hole whenever the hole lies between that + // entry's ideal bucket and where it sits. That preserves the only + // invariant `probe` needs -- every key reachable by a forward scan from + // its own ideal bucket -- which is why no tombstone is required. // // Distances are measured forward from `ideal` and masked, so wrapping - // is handled without a signed comparison. + // needs no signed comparison. let mut i = (hole + 1) & self.mask; - while let Some(e) = self.buckets[i].as_ref() { - let ideal = (e.hash as usize) & self.mask; + loop { + let b = self.buckets[i]; + if b.handle == EMPTY { + break; + } + let ideal = (b.hash as usize) & self.mask; let to_hole = hole.wrapping_sub(ideal) & self.mask; let to_here = i.wrapping_sub(ideal) & self.mask; if to_hole < to_here { - self.buckets[hole] = self.buckets[i].take(); + self.buckets[hole] = b; + self.buckets[i].handle = EMPTY; hole = i; } i = (i + 1) & self.mask; @@ -491,7 +520,7 @@ impl ClientHandle { pub struct ClientTable { /// Key to slot. The only hashed structure, and the reason a request costs /// one hash instead of several. - index: Index, + index: Index, /// Records in stable storage. Slots are handed out from `free` and never /// move, which is what lets the ordering below be pointers rather than /// comparisons. @@ -600,7 +629,7 @@ impl ClientTable { // deletion cheap (measured -6.45% Ir on the churn arm against 0.5), and // it is paid in memory. A caller sizing a table for a large population // needs to see it here rather than discover it in RSS. - core::mem::size_of::>() + 4 * core::mem::size_of::>>() + core::mem::size_of::>() + 4 * core::mem::size_of::() } pub fn len(&self) -> usize { @@ -612,7 +641,7 @@ impl ClientTable { } pub fn get(&self, key: &K) -> Option<&ClientRecord> { - let i = self.index.get(key)?; + let i = self.index.get(key, &self.slots)?; Some(&self.slots[i as usize].record) } @@ -645,8 +674,10 @@ impl ClientTable { return; } self.unlink(victim); - let key = self.slots[victim as usize].key.clone(); - self.index.remove(&key); + let ClientTable { index, slots, .. } = self; + // No clone: `index` is borrowed mutably and `slots` immutably, and they + // are different fields, so destructuring `self` lets both live at once. + index.remove(&slots[victim as usize].key, slots); self.free.push(victim); self.stats.evicted += 1; } @@ -733,7 +764,7 @@ impl ClientTable { // client we have never seen. The previous form hashed three times per // request: `contains_key`, then `touch`'s `get_mut`, then a final // `get_mut` to reach the record. - let slot = match self.index.get(key) { + let slot = match self.index.get(key, &self.slots) { Some(i) => { self.touch(i); i @@ -743,7 +774,8 @@ impl ClientTable { self.evict_one(); } let i = self.alloc_slot(key.clone(), ClientRecord::new(now, self.config.burst)); - self.index.insert(key.clone(), i); + let ClientTable { index, slots, .. } = self; + index.insert(key, i, slots); i } }; @@ -792,7 +824,7 @@ impl ClientTable { /// timestamp. That is unforgeable in the useful sense: only a client that /// actually saw our last response knows it. pub fn response_mode(&mut self, key: &K, request_origin: NtpTimestamp) -> ResponseMode { - match self.index.get(key) { + match self.index.get(key, &self.slots) { Some(i) => { let handle = self.handle_for(i); self.response_mode_at(handle, request_origin) @@ -836,7 +868,7 @@ impl ClientTable { /// Record what we received and what we told the client, after answering. pub fn note_response(&mut self, key: &K, receive: NtpTimestamp, receive_sent: NtpTimestamp) { - if let Some(i) = self.index.get(key) { + if let Some(i) = self.index.get(key, &self.slots) { let handle = self.handle_for(i); self.note_response_at(handle, receive, receive_sent); } @@ -861,7 +893,7 @@ impl ClientTable { /// timestamp the basic exchange cannot report because the packet has not /// left yet when its own transmit field is written. pub fn note_transmit(&mut self, key: &K, transmit: NtpTimestamp) { - if let Some(i) = self.index.get(key) { + if let Some(i) = self.index.get(key, &self.slots) { self.slots[i as usize].record.last_transmit = Some(transmit); } } From 871a2ae72b19db37f4281ae47df83b0a4b227147 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Oct 2026 20:18:10 -0700 Subject: [PATCH 12/15] chore: release 0.2.1 -- the performance pass, behaviour-neutral Six deterministic Ir wins in the client filter and the server's client table. client_path -24,968,715 (-11.38%); hot_path -3.71% steady and -10.39% under churn; mru_report -9.81%. No behaviour changed, and the corpus proves it rather than the commit asserting it: TIMECORP S1/S6/S8 at 31 seeds are byte-identical to 0.2.0, and S12a/b/c server-load counts match including 1,021,672 evictions in S12b. cargo semver-checks reports no semver update required, so this is a patch. bytes_per_client() now reports 160 rather than 150 -- the index holds four buckets per client instead of one entry. Signature unchanged. Also adds tools/validate-release.sh, the pre-release gate this release was run through: 17 checks over a clean git worktree of the exact commit (never a working tree, which can hold another session's uncommitted edits). Two of its own checks were wrong on the first run and are fixed: it tripped over itself in the dirty check, and it grepped for 'unsafe' across all crates when rusty_time-clock legitimately lifts that lint for the syscall boundary and the compiler is the authority anyway. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.lock | 18 ++-- Cargo.toml | 12 +-- crates/rusty_time-alloc/CHANGELOG.md | 8 ++ crates/rusty_time-api/CHANGELOG.md | 8 ++ crates/rusty_time-clock/CHANGELOG.md | 8 ++ crates/rusty_time-core/CHANGELOG.md | 44 +++++++++ crates/rusty_time-ctl/CHANGELOG.md | 8 ++ crates/rusty_time-daemon/CHANGELOG.md | 8 ++ crates/rusty_time-nts/CHANGELOG.md | 8 ++ crates/rusty_time-sim/CHANGELOG.md | 8 ++ crates/rusty_time-wasm/CHANGELOG.md | 8 ++ tools/validate-release.sh | 129 ++++++++++++++++++++++++++ 12 files changed, 252 insertions(+), 15 deletions(-) create mode 100644 tools/validate-release.sh diff --git a/Cargo.lock b/Cargo.lock index 7549493..0aa4305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1942,14 +1942,14 @@ source = "git+https://github.com/Remade-With-Rust/rusty_json_turbo?branch=master [[package]] name = "rusty_time-alloc" -version = "0.2.0" +version = "0.2.1" dependencies = [ "rusty_alloc-api", ] [[package]] name = "rusty_time-api" -version = "0.2.0" +version = "0.2.1" dependencies = [ "serde", "serde_json", @@ -1957,7 +1957,7 @@ dependencies = [ [[package]] name = "rusty_time-clock" -version = "0.2.0" +version = "0.2.1" dependencies = [ "libc", "rusty_time-core", @@ -1966,11 +1966,11 @@ dependencies = [ [[package]] name = "rusty_time-core" -version = "0.2.0" +version = "0.2.1" [[package]] name = "rusty_time-ctl" -version = "0.2.0" +version = "0.2.1" dependencies = [ "rusty_time-alloc", "rusty_time-api", @@ -1980,7 +1980,7 @@ dependencies = [ [[package]] name = "rusty_time-daemon" -version = "0.2.0" +version = "0.2.1" dependencies = [ "aes-gcm 0.11.1", "argon2", @@ -1997,7 +1997,7 @@ dependencies = [ [[package]] name = "rusty_time-nts" -version = "0.2.0" +version = "0.2.1" dependencies = [ "aes-siv", "getrandom 0.4.3", @@ -2009,7 +2009,7 @@ dependencies = [ [[package]] name = "rusty_time-sim" -version = "0.2.0" +version = "0.2.1" dependencies = [ "rusty_time-alloc", "rusty_time-core", @@ -2019,7 +2019,7 @@ dependencies = [ [[package]] name = "rusty_time-wasm" -version = "0.2.0" +version = "0.2.1" dependencies = [ "rusty_time-core", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 4cd2d22..098ead0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ exclude = ["bare-metal/esp32s3"] [workspace.package] -version = "0.2.0" +version = "0.2.1" edition = "2024" license = "MIT OR Apache-2.0" repository = "https://github.com/remade-with-rust/rusty_time" @@ -29,11 +29,11 @@ rust-version = "1.95" unsafe_code = "deny" [workspace.dependencies] -rusty_time-core = { path = "crates/rusty_time-core", version = "0.2.0" } -rusty_time-nts = { path = "crates/rusty_time-nts", version = "0.2.0" } -rusty_time-clock = { path = "crates/rusty_time-clock", version = "0.2.0" } -rusty_time-api = { path = "crates/rusty_time-api", version = "0.2.0" } -rusty_time-alloc = { path = "crates/rusty_time-alloc", version = "0.2.0" } +rusty_time-core = { path = "crates/rusty_time-core", version = "0.2.1" } +rusty_time-nts = { path = "crates/rusty_time-nts", version = "0.2.1" } +rusty_time-clock = { path = "crates/rusty_time-clock", version = "0.2.1" } +rusty_time-api = { path = "crates/rusty_time-api", version = "0.2.1" } +rusty_time-alloc = { path = "crates/rusty_time-alloc", version = "0.2.1" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/rusty_time-alloc/CHANGELOG.md b/crates/rusty_time-alloc/CHANGELOG.md index 8e68289..2db025a 100644 --- a/crates/rusty_time-alloc/CHANGELOG.md +++ b/crates/rusty_time-alloc/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.1] - 2026-10-08 + +### Changed + +- Version moved to 0.2.1 with the rest of the workspace. **Nothing in this + crate's own API or behaviour changed.** The release is a performance pass in + `rusty_time-core`; see that crate's changelog. + ## [0.2.0] - 2026-09-09 ### Changed diff --git a/crates/rusty_time-api/CHANGELOG.md b/crates/rusty_time-api/CHANGELOG.md index f7b5710..2b534ec 100644 --- a/crates/rusty_time-api/CHANGELOG.md +++ b/crates/rusty_time-api/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.1] - 2026-10-08 + +### Changed + +- Version moved to 0.2.1 with the rest of the workspace. **Nothing in this + crate's own API or behaviour changed.** The release is a performance pass in + `rusty_time-core`; see that crate's changelog. + ## [0.2.0] - 2026-09-09 ### Changed diff --git a/crates/rusty_time-clock/CHANGELOG.md b/crates/rusty_time-clock/CHANGELOG.md index f7b5710..2b534ec 100644 --- a/crates/rusty_time-clock/CHANGELOG.md +++ b/crates/rusty_time-clock/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.1] - 2026-10-08 + +### Changed + +- Version moved to 0.2.1 with the rest of the workspace. **Nothing in this + crate's own API or behaviour changed.** The release is a performance pass in + `rusty_time-core`; see that crate's changelog. + ## [0.2.0] - 2026-09-09 ### Changed diff --git a/crates/rusty_time-core/CHANGELOG.md b/crates/rusty_time-core/CHANGELOG.md index 4509e0a..faeb481 100644 --- a/crates/rusty_time-core/CHANGELOG.md +++ b/crates/rusty_time-core/CHANGELOG.md @@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.1] - 2026-10-08 + +### Performance + +Six deterministic wins in the client filter and the server's client table, +measured with callgrind Ir (exact: the same binary re-runs to zero) and gated +byte-identical throughout. + +| instrument | before | after | delta | +|---|---:|---:|---:| +| `client_path` | 219,439,512 | 194,470,797 | **-24,968,715 (-11.38%)** | +| `hot_path` (steady) | 47,424,979 | 45,664,884 | **-1,760,095 (-3.71%)** | +| `hot_path` (churn) | 75,642,533 | 67,786,128 | **-7,856,405 (-10.39%)** | +| `mru_report` | 5,804,467 | 5,234,883 | **-569,584 (-9.81%)** | + +- `regress` called `wls_fit` twice on identical data (-17,918,178). +- The spike filter recomputed residuals `spike_threshold` had already produced; + they travel as a `u64` keep mask now (-2,441,711). +- A median taken by full sort now uses `select_nth_unstable_by` (-3,363,768). +- `2f64.powi(k)` builds the exponent field directly instead of calling + `__powidf2` 16,000 times a run (-690,184). +- The regime-change trim advances a cursor instead of front-draining up to four + times (-557,376). +- The client index is open-addressed with backward-shift deletion (so no + tombstones) and does not duplicate the key, which `HashMap` had to. Costs + +10 bytes/client (150 -> 160). + +**No behaviour changed.** TIMECORP S1/S6/S8 at 31 seeds are byte-identical to +0.2.0, and S12a/b/c server-load counts are identical including 1,021,672 +evictions in S12b. `cargo semver-checks`: no semver update required. + +### Changed + +- `ClientTable::bytes_per_client()` reports 160 rather than 150, because the + index now holds four buckets per client instead of one entry. The signature + is unchanged; the number is honest. + +### Added + +- `HOT_PATH_EVICT`, a churn arm for the `hot_path` bench. The default arm never + evicts, so it could not gate any change to the client index -- and at a 0.5 + load factor the new index measured a clean win there while costing +9.86% + under eviction. The arm is what caught it. + ## [0.2.0] - 2026-09-09 ### Added diff --git a/crates/rusty_time-ctl/CHANGELOG.md b/crates/rusty_time-ctl/CHANGELOG.md index f7b5710..2b534ec 100644 --- a/crates/rusty_time-ctl/CHANGELOG.md +++ b/crates/rusty_time-ctl/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.1] - 2026-10-08 + +### Changed + +- Version moved to 0.2.1 with the rest of the workspace. **Nothing in this + crate's own API or behaviour changed.** The release is a performance pass in + `rusty_time-core`; see that crate's changelog. + ## [0.2.0] - 2026-09-09 ### Changed diff --git a/crates/rusty_time-daemon/CHANGELOG.md b/crates/rusty_time-daemon/CHANGELOG.md index f7b5710..2b534ec 100644 --- a/crates/rusty_time-daemon/CHANGELOG.md +++ b/crates/rusty_time-daemon/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.1] - 2026-10-08 + +### Changed + +- Version moved to 0.2.1 with the rest of the workspace. **Nothing in this + crate's own API or behaviour changed.** The release is a performance pass in + `rusty_time-core`; see that crate's changelog. + ## [0.2.0] - 2026-09-09 ### Changed diff --git a/crates/rusty_time-nts/CHANGELOG.md b/crates/rusty_time-nts/CHANGELOG.md index f7b5710..2b534ec 100644 --- a/crates/rusty_time-nts/CHANGELOG.md +++ b/crates/rusty_time-nts/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.1] - 2026-10-08 + +### Changed + +- Version moved to 0.2.1 with the rest of the workspace. **Nothing in this + crate's own API or behaviour changed.** The release is a performance pass in + `rusty_time-core`; see that crate's changelog. + ## [0.2.0] - 2026-09-09 ### Changed diff --git a/crates/rusty_time-sim/CHANGELOG.md b/crates/rusty_time-sim/CHANGELOG.md index f7b5710..2b534ec 100644 --- a/crates/rusty_time-sim/CHANGELOG.md +++ b/crates/rusty_time-sim/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.1] - 2026-10-08 + +### Changed + +- Version moved to 0.2.1 with the rest of the workspace. **Nothing in this + crate's own API or behaviour changed.** The release is a performance pass in + `rusty_time-core`; see that crate's changelog. + ## [0.2.0] - 2026-09-09 ### Changed diff --git a/crates/rusty_time-wasm/CHANGELOG.md b/crates/rusty_time-wasm/CHANGELOG.md index f7b5710..2b534ec 100644 --- a/crates/rusty_time-wasm/CHANGELOG.md +++ b/crates/rusty_time-wasm/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.1] - 2026-10-08 + +### Changed + +- Version moved to 0.2.1 with the rest of the workspace. **Nothing in this + crate's own API or behaviour changed.** The release is a performance pass in + `rusty_time-core`; see that crate's changelog. + ## [0.2.0] - 2026-09-09 ### Changed diff --git a/tools/validate-release.sh b/tools/validate-release.sh new file mode 100644 index 0000000..519a09a --- /dev/null +++ b/tools/validate-release.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# Pre-release validator for rusty_time. +# +# Answers one question: would shipping THIS commit damage anything? It is run +# against a clean `git worktree` of the exact commit, never against a working +# tree, because a working tree can hold someone else's uncommitted edits and +# those are not what gets published. +# +# Every check is fail-loud with its own line. A `cmd | grep` that swallows an +# exit status is how a gate reports "fine" on a broken build, so nothing here +# pipes a build into a filter without checking ${PIPESTATUS[0]}. +set -u + +PASS=0 +FAIL=0 +WARN=0 + +ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; PASS=$((PASS+1)); } +bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; FAIL=$((FAIL+1)); } +warn() { printf ' \033[33mWARN\033[0m %s\n' "$1"; WARN=$((WARN+1)); } + +run() { # run