Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
b8d39c6
Swap serde_json for the house fork (rusty_json_turbo)
tim-almond-house Sep 10, 2026
00b3965
perf(core): three deterministic wins in the filter hot path (-10.0% Ir)
tim-almond-house Oct 9, 2026
d0376b3
docs: round 2 -- A3 refuted in both forms, A8 is the harness, B16 ref…
tim-almond-house Oct 9, 2026
afa3289
perf(core): A2 lands as a keep mask -- -2,441,711 Ir (-1.24%)
tim-almond-house Oct 9, 2026
df11919
perf(core): A7 -- one drain instead of four in the trim loop (-557,37…
tim-almond-house Oct 9, 2026
217bba7
docs: round 3 -- A2 and A7 overturned as wins, B13's ceiling retracted
tim-almond-house Oct 9, 2026
facca60
docs: round 4 -- B15 refuted by the instrument; all twenty veins closed
tim-almond-house Oct 9, 2026
e5fa0a8
bench(core): a churn arm for hot_path -- the steady arm reports 'evic…
tim-almond-house Oct 9, 2026
afe8b21
perf(core): B15 -- an open-addressed client index (-2.20% steady, -6.…
tim-almond-house Oct 9, 2026
d631761
docs: round 5 -- A4 refuted on arithmetic; all twenty veins closed
tim-almond-house Oct 9, 2026
61cbc28
perf(core): the client index drops its duplicate key (-3.71%/-10.39% …
tim-almond-house Oct 9, 2026
871a2ae
chore: release 0.2.1 -- the performance pass, behaviour-neutral
tim-almond-house Oct 9, 2026
ab8dc61
perf(core): round 6 -- ten more veins, two wins (-808,139 / -317,822 Ir)
tim-almond-house Oct 9, 2026
3a97511
docs: round 6 performance in the ledger and the README
tim-almond-house Oct 9, 2026
211ca05
chore: release 0.3.0 -- BREAKING, and semver-checks did not see why
tim-almond-house Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 29 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 28 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ members = [
exclude = ["bare-metal/esp32s3"]

[workspace.package]
version = "0.2.0"
version = "0.3.0"
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/remade-with-rust/rusty_time"
Expand All @@ -29,14 +29,36 @@ 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.3.0" }
rusty_time-nts = { path = "crates/rusty_time-nts", version = "0.3.0" }
rusty_time-clock = { path = "crates/rusty_time-clock", version = "0.3.0" }
rusty_time-api = { path = "crates/rusty_time-api", version = "0.3.0" }
rusty_time-alloc = { path = "crates/rusty_time-alloc", version = "0.3.0" }
serde = { version = "1", features = ["derive"] }
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" }
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,14 @@ error of the **predicted offset** rather than of the slope, which took S8 from
chrony before that and are now level.

Instruction cost, deterministic and internal (not a chrony comparison):
**237 Ir per served request**, **13,613 Ir per client discipline step** — within
1 Ir of `0.1.7` despite the multi-source rework.
**224.3 Ir per served request**, **12,137.5 Ir per client discipline step**,
measured with callgrind on three seeded harnesses whose checksums gate every
change. Six rounds of instruction-count work took the server from 3,053.6 and
the client from 25,627; the last round's two wins were a representation change
(three `Option<NtpTimestamp>` fields to a zero sentinel, −1.77% on the server)
and one inlined pass-through frame (−0.16% on the client). Across those rounds
the "clever local rewrite" class went **0-for-8** — every win was a deletion, a
representation change, or a libm swap.

Three honest notes. Earlier per-scenario accuracy wins reported here were
**withdrawn**: they came from comparing medians on a rig whose unchanged control
Expand Down
99 changes: 99 additions & 0 deletions corpus/LEDGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -2696,3 +2696,102 @@ long window: it can see the slope's variance and cannot see its bias.

Closing that needs a criterion with a bias signal in it, not another sweep.
Recorded as the open item, with the measurement that will falsify it.

## Round 6: ten more veins on the two censuses nobody had taken

`docs/plans/instruction-veins.md` (round 6), `tools/perf/codec_ir.sh` (new).

The twenty veins of rounds 1-5 were closed. Ten more came from the two censuses
those rounds never ran — **call counts and caller-callee EDGES**
(`instruction-counting` §5b/§5c, where rounds 1-5 used self-cost only) — plus a
reading of the `no_std` codec leaf, which had no instrument at all.

### Kept, both gated byte-identical

| vein | change | instrument | Ir |
|---|---|---|---:|
| N3 | `ClientRecord`'s three `Option<NtpTimestamp>` to `NtpTimestamp`, zero = unset | hot_path | **-808,139 (-1.77%)** |
| N2a | `#[inline(always)]` on `MultiController::estimate` — one caller, one callee, 37.2 Ir/call of frame | client_path | **-317,822 (-0.16%)** |

**Server: 228.3 -> 224.3 Ir per served request. Client: 12,154.3 -> 12,137.5 Ir
per discipline step.** Gates: `hot_path` `0xc0452ab605328800`, `client_path`
`f456d8bf8ff664d5`, `codec` `0x99f61c51fcc31496` — all unchanged; work parity
exact on all three; 218 tests, clippy `-D warnings`, fmt, and the `no_std` leaf
on `thumbv7em-none-eabihf` and `riscv32imac-unknown-none-elf`.

N3 is a representation change, not a rewrite: `NtpTimestamp` is a bare `u64`
with no niche, so `Option<NtpTimestamp>` is sixteen bytes for eight of payload
and every read tests a discriminant. RFC 5905 already spells "unset" as an
all-zero timestamp. What makes the sentinel safe rather than merely convenient
is that the interleaved path's zero-origin rejection
(`!request_origin.is_zero()`) was **already explicit and independent of the
`Option`** — that test is the security property, not the discriminant. 24 bytes
off every record as a side effect.

### Refuted, measured, numbers left in the source

| probe | Ir | why |
|---|---:|---|
| N9 `from_bits` as a total lookup table | **+2,048,014 (+4.79%)** | LLVM already emits arithmetic — the discriminants ARE the wire values — so the table only added a load. The magnitude matched `rusty-compiler-leverage` B5's recorded bitmask refutation (+2.03M) |
| N8 `parse` via one `&[u8; 48]` (`first_chunk`) | **+0, exactly** | §6: a byte-identical total means the compiler already did it. Cost moved buckets (ntp.rs -1,024,000, `core/src/slice/mod.rs` +1,600,000), total bit-identical. **B18's "already at its floor" was right** |
| N9' `#[repr(u8)]` + `self as u8` for `bits()` | +0 | already a cast |
| N2b bind `registers[index]` once, not three times | +0 | already CSE'd, as `admit_handle` was in round 1 |
| N1 `#[inline(always)]` on `residual_sd` | +0 alone, +0 combined | LLVM had already made the call; attribute removed |

**The clever-local-rewrite class is now 0-for-8 across six rounds** (1-5 went
0-for-6; N8 and N9 make eight). Every win in six rounds has been a deletion, a
representation change, or a libm swap.

### Instruments

- **A third one exists now.** `benches/codec.rs` + `tools/perf/codec_ir.sh`
prices `ntp.rs` on its own — bytes in, bytes out, no table, no filter, no
clock, which is the different corpus shape §4 asks for. It matters because
`ntp.rs` is the `no_std` leaf: on a Cortex-M4F, an RV32 part or in the wasm
client it is not 1.27% of the work, it is ~all of it.
- **Its own tap was 63% of its first measurement.** Nineteen FNV folds per
packet. The fix was not cheaper mixing but fewer mixed values: `write` is the
inverse of `parse` and re-emits all 48 bytes from all 13 fields, so folding
the six OUTPUT words covers every field transitively. 19 mixes to 9, total
67,297,513 to 42,721,512, and the product's own code went from the fourth row
to the second (23.6 Ir/packet).
- **`ir.sh` and `client_ir.sh` now `touch` their sources before building.**
Caught live: three consecutive identical A/B results, and one run that read
the pre-A1 Ir with A1 sitting in the tree. Cargo's fingerprint across the
`/mnt` drvfs mount does not reliably notice an edit, so a run can measure a
stale binary — the trap `codec-memory-copies` §4 names, on this rig.
- **The `client_path` harness tax, never computed:** 3,661,681 Ir = **1.67%** =
229 Ir/step (main's own 1,943,431 plus the 1,718,250 of `log` it calls).
C19 did this for `hot_path` (13.92%) and gated section B on it; section A had
no equivalent. It is small, so section A's shares stand.

### Two corrections to rounds 1-5

- **A8 was not a vein, and its figure was half.** `benches/client_path.rs:66`
is `-u.ln()`, and there is **no `ln`/`log` call anywhere in the product
source**. 32,000 calls (two per step), 1,718,250 Ir — double the recorded
16,000 / 859,160 — and all of it the harness's own noise generator. The
caller-callee edge said so in one line (`client_path::main -> log`); self-cost
alone cannot name an owner. Round 2's commit message already said "A8 is the
harness" while the ceiling table still priced it as real work.
- **B18 overstated its conclusion.** "`ntp.rs` is already at its floor" was read
off an instrument where `parse`/`write` inline into `hot_path::main` with 52%
of events unattributed. N8 has now turned that into a measurement rather than
an assumption — and it came out right.

### A coverage hole worth more than either win

The two most tempting veins in the file — a surviving full `sort_by` and a
double allocation — sit behind config knobs that default to `0.0` and `false`.
Checking reach demoted both correctly. The deeper find was that one of those
branches had **no test at all**, so a twelve-line rewrite of its inner loop was
green against 217 tests without executing a line of it.
`density_weighting_factors_span_the_window` now pins the half-gap rule
(computed independently in the test, not recorded from the code) and enters the
branch.

### One law carried forward

N3 won 1.77% on `hot_path` and cost ~50,000 Ir on `client_path` — same crate,
an unrelated function, codegen shifted. `instruction-counting` §10 says measure
every change on every instrument; quote both numbers or neither.
8 changes: 8 additions & 0 deletions crates/rusty_time-alloc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 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
Expand Down
8 changes: 8 additions & 0 deletions crates/rusty_time-api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 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
Expand Down
8 changes: 8 additions & 0 deletions crates/rusty_time-clock/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 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
Expand Down
64 changes: 64 additions & 0 deletions crates/rusty_time-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 2026-10-08

### BREAKING

- `ClientRecord`'s three timestamp fields change type:
`pub last_receive`, `pub last_transmit` and `pub last_receive_sent` are now
`NtpTimestamp` rather than `Option<NtpTimestamp>`, with
`NtpTimestamp::ZERO` meaning "unset" — which is how RFC 5905 already spells
it on the wire. `Option<NtpTimestamp>` was sixteen bytes for eight bytes of
payload, because `NtpTimestamp` is a bare `u64` with no niche, and every read
tested a discriminant.

**Migration:** `if let Some(ts) = rec.last_receive` becomes
`if !rec.last_receive.is_zero()`, and `rec.last_receive` is the value
directly.

This is why the release is 0.3.0 and not 0.2.1. Note that
`cargo semver-checks` reports *no semver update required* for it — it has no
lint for a public field's TYPE changing — so the bump was decided by reading
the diff, not by the tool. Do not trust that tool alone on a field-type
change.

### 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
Expand Down
Loading
Loading