# Baseline numbers captured before T0 changes # Hardware: lrrr.local / Apple M5 Pro # Date: 2026-04-24 Batch::iteration 29.840 µs Gaussian::add 219.58 ps Gaussian::sub 219.41 ps Gaussian::mul 1.568 ns ← hot path; target ≥1.5× improvement Gaussian::div 1.572 ns ← hot path; target ≥1.5× improvement Gaussian::pi 262.89 ps Gaussian::tau 262.47 ps Gaussian::pi_tau_combined 219.40 ps # After T0 (2026-04-24, same hardware) Batch::iteration 21.253 µs (1.40× — below 3× target; see post-mortem) Gaussian::add 218.62 ps (1.00× — unchanged, Add/Sub use moment form) Gaussian::sub 220.15 ps (1.00×) Gaussian::mul 218.69 ps (7.17× — nat-param: now two f64 adds, no sqrt) Gaussian::div 218.64 ps (7.19× — nat-param: now two f64 subs, no sqrt) Gaussian::pi 263.19 ps (1.00× — now a field read, same cost) Gaussian::tau 263.51 ps (1.00× — now a field read, same cost) Gaussian::pi_tau_combined 219.13 ps (1.00×) # Post-mortem: Batch::iteration 1.40× vs. 3× target # # Root cause: the bench has 100 tiny 2-team events. Each event still allocates # ~10 Vecs per iteration (down from ~18). The arena covers teams/diffs/ties/margins # (was 4 Vecs, now 0 new allocs) but the following remain: # - within_priors() returns Vec>>: 3 Vecs per event (300 total) # - event.outputs() returns Vec: 1 Vec per event (100 total) # - sort_perm() allocates 2 scratch Vecs: 200 total # - Game::likelihoods = collect() allocates Vec>: 4 Vecs (400 total) # Total remaining: ~1000 allocs per iteration call vs. ~1800 before (44% reduction). # # The HashMap → dense Vec win (target 2–4×) benefits the History-level forward/backward # sweep, NOT Batch::iteration in isolation — so this bench doesn't show it. # # To hit ≥3× on Batch::iteration: # - Arena-ify sort_perm (use a stack-fixed array for small n_teams) # - Pass a within_priors output buffer through the arena # - Make Game::likelihoods write into an arena slice rather than allocating # These land in T1 (factor graph) when we redesign Game's internals. # After T1 (2026-04-24, same hardware) Batch::iteration 23.010 µs (1.08× vs T0 21.253 µs — slight regression) Gaussian::add 231.23 ps (unchanged) Gaussian::sub 235.38 ps (unchanged) Gaussian::mul 234.55 ps (unchanged — nat-param storage) Gaussian::div 233.27 ps (unchanged) Gaussian::pi 272.68 ps (unchanged) Gaussian::tau 272.73 ps (unchanged) Gaussian::pi_tau_combined 234.xx ps (unchanged) # Notes: # - Batch::iteration 23.0 µs vs target ≤ 21.5 µs (8% above target). # Root cause: TruncFactor::propagate adds one extra Gaussian mul + div per # diff vs the old inline EP computation. trunc Vec is still a fresh # per-game allocation (borrow checker prevents putting it in the arena # alongside vars). These are addressable in T2. # - arena.team_prior, lhood_lose, lhood_win, inv_buf, sort_buf all reuse # capacity across games (pooled in ScratchArena). sort_perm() allocation # eliminated. message.rs deleted. # - Gaussian operations unchanged vs T0. # - All 53 tests pass. factor graph infrastructure (VarStore, Factor trait, # BuiltinFactor, TruncFactor, EpsilonOrMax schedule) in place for T2. # After T2 (2026-04-24, same hardware) Batch::iteration 21.36 µs (1.07× vs T1 22.88 µs — 7% improvement) Gaussian::add 218.97 ps (unchanged) Gaussian::sub 218.58 ps (unchanged) Gaussian::mul 218.59 ps (unchanged) Gaussian::div 218.57 ps (unchanged) Gaussian::pi 264.20 ps (unchanged) Gaussian::tau 260.80 ps (unchanged) # Notes: # - API-only tier; hot inference path unchanged. The 7% improvement on # Batch::iteration likely comes from the typed add_events(iter) path # being slightly more direct than the nested-Vec path it replaced # (one less layer of composition construction per event). # - Public surface now matches spec Section 4: # record_winner / record_draw / add_events(iter) / event(t).team().commit() # converge() -> Result # learning_curve(&K) / learning_curves() / current_skill(&K) # log_evidence() / log_evidence_for(&[&K]) # predict_quality / predict_outcome # Game::ranked / one_v_one / free_for_all / custom # factors module (pub Factor/Schedule/VarStore/EpsilonOrMax/BuiltinFactor) # - Breaking type renames: Batch→TimeSlice, Player→Rating, Agent→Competitor, # IndexMap→KeyTable. # - Generic over T: Time (default i64), D: Drift, O: Observer, # K: Eq + Hash + Clone (default &'static str). # - Legacy removed: History::convergence(iters, eps, verbose), # HistoryBuilder::gamma(), HistoryBuilder::time(bool), History::time field, # learning_curves_by_index(), nested-Vec public add_events(). # - 90 tests green: 68 lib + 10 api_shape + 6 game + 4 record_winner + # 2 equivalence. # After T3 (2026-04-24, same hardware) Batch::iteration (seq, no rayon) 23.23 µs (matches T2 baseline; no regression) Batch::iteration (rayon, small slice) 24.57 µs (within noise; small workloads pay rayon overhead) Gaussian::add 236.62 ps (unchanged) Gaussian::sub 236.43 ps (unchanged) Gaussian::mul 237.05 ps (unchanged) Gaussian::div 236.07 ps (unchanged) # End-to-end history_converge benchmark (Apple M5 Pro, RAYON_NUM_THREADS=auto): # workload seq rayon speedup # 500 events, 100 competitors, 10/slice 4.03 ms 4.24 ms 1.0x # 2000 events, 200 competitors, 20/slice 20.18 ms 19.82 ms 1.0x # 5000 events, 50000 competitors, 1 slice 11.88 ms 9.10 ms 1.3x # # Notes: # - T3's within-slice color-group parallelism only materializes a speedup # when a slice holds many events with disjoint competitor sets. Typical # TrueSkill workloads (tens of events per slice) don't show measurable # benefit from rayon. # - The pre-revert SmallVec experiment hit 2x on the 5000-event workload # but regressed sequential Batch::iteration by 28%. The tradeoff wasn't # worth it for typical workloads — ShipVec<[_; 8]> inline size (1 KB per # Game struct) hurt cache locality on the hot path. # - Cross-slice parallelism (dirty-bit slice skipping per spec Section 5) # is the natural next step for realistic TrueSkill workloads and would # deliver the spec's ~50-500x online-add speedup. Deferred to T4+. # - Determinism verified: tests/determinism.rs asserts bit-identical # posteriors across RAYON_NUM_THREADS={1, 2, 4, 8}. # - Send + Sync bounds added on Time, Drift, Observer, Factor, Schedule. # - Rayon is opt-in via `--features rayon`. Default build is unchanged from T2.