Skip to content

Commit 43c2d65

Browse files
zackeesclaude
andauthored
feat(library-selection): #215 add resolve_warm criterion bench (P-01 mini) (#216)
Mirrors resolve_cold.rs but pre-populates a KvStore against the same ~30-library MiniFramework fixture and times only the cache-hit path through resolve_cached(). Each bench iteration asserts from_cache so a silent regression to the miss path can never be measured as warm work. Wires the bench into the .github/workflows/bench-205.yml matrix as a third lane (P-01-mini, baseline TBD — no failure gate yet) and updates bench/README.md, the fastled-examples/ placeholder, and the per-crate benches/README.md to reflect that the synthetic warm baseline now exists; the real per-board FastLED matrix remains future work. Local numbers (Windows, debug shell): cold ~5.0 ms, warm ~1.7 ms over the same MiniFramework tree. Closes #215 Refs #205 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eb4e6de commit 43c2d65

6 files changed

Lines changed: 179 additions & 23 deletions

File tree

.github/workflows/bench-205.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ jobs:
2828
- crate: fbuild-library-select
2929
bench: resolve_cold
3030
target: P-02 (<= 200 ms cold for typical project)
31+
- crate: fbuild-library-select
32+
bench: resolve_warm
33+
target: P-01-mini (warm cache against MiniFramework, baseline TBD)
3134
steps:
3235
- uses: actions/checkout@v6
3336
- uses: astral-sh/setup-uv@v3

bench/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,22 @@ Per-crate criterion benches live alongside their crate, e.g.:
77

88
- `crates/fbuild-header-scan/benches/scan_throughput.rs`
99
- `crates/fbuild-library-select/benches/resolve_cold.rs`
10+
- `crates/fbuild-library-select/benches/resolve_warm.rs`
1011

1112
Run those with:
1213

1314
```bash
1415
uv run soldr cargo bench -p fbuild-library-select --bench resolve_cold
16+
uv run soldr cargo bench -p fbuild-library-select --bench resolve_warm
1517
uv run soldr cargo bench -p fbuild-header-scan --bench scan_throughput
1618
```
1719

1820
## Subdirectories
1921

20-
- [`fastled-examples/`](fastled-examples/README.md)placeholder for the
21-
warm-cache library-selection harness across the FastLED examples matrix
22-
(`FastLED/fbuild#205` AC#5, P-01). Awaits Phase 4 zccache K/V memoization
23-
before there's a warm path to measure.
22+
- [`fastled-examples/`](fastled-examples/README.md)reserved for the
23+
real-FastLED warm-cache library-selection matrix (`FastLED/fbuild#205`
24+
AC#5, P-01) once `~/dev/fastled` is wired in. The synthetic warm-path
25+
baseline already lives in `crates/fbuild-library-select/benches/resolve_warm.rs`.
2426

2527
Other end-to-end matrices (whole-build wall-clock, deploy+flash latency,
2628
emulator boot) may join this directory in the future. Each subdirectory

bench/fastled-examples/README.md

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@ criterion **AC#5 / P-01**:
99
1010
## Status: empty placeholder
1111

12-
There is no harness here yet, and on purpose. P-01 measures the **warm**
13-
path through the resolver, which depends on the zccache K/V memoization
14-
delivered by `#205` Phase 4 (gated on `zackees/zccache#130`). Until that
15-
lands, every `resolve()` call is cold and there is no warm number to
16-
gate against. Adding a "warm-ish" harness today would just measure cold
17-
work twice and produce a misleading baseline.
12+
There is no harness in this directory yet — the real per-board, per-example
13+
matrix needs a checked-out FastLED tree (`~/dev/fastled`) and orchestrator
14+
wiring that routes through `resolve_cached`. That work is tracked
15+
separately. The synthetic warm baseline (`MiniFramework`-backed cache hit,
16+
no real FastLED) already exists at
17+
[`../../crates/fbuild-library-select/benches/resolve_warm.rs`](../../crates/fbuild-library-select/benches/resolve_warm.rs)
18+
and is the first-pass regression guard for the cache-hit path.
1819

19-
When Phase 4 ships and a `zccache` release is cut, a follow-up PR drops
20-
the actual harness into this directory and wires it into CI.
20+
Phase 4 K/V memoization itself shipped in PR #212, so the warm path is
21+
real and measurable today; what is missing here is the multi-board,
22+
real-sketch matrix that AC#5 requires.
2123

22-
## The plan once Phase 4 lands
24+
## The plan once the FastLED tree is wired in
2325

2426
1. Iterate the FastLED examples tree (`~/dev/fastled/examples/**`) under
2527
each supported board: at minimum `teensyLC`, `teensy30`, `teensy41`,
@@ -38,29 +40,30 @@ the actual harness into this directory and wires it into CI.
3840
4. Emit a structured JSON report (`bench/fastled-examples/report.json`)
3941
that future PR comments can diff. Format TBD with the harness.
4042

41-
## Running a partial version today
43+
## Running the synthetic mini benches today
4244

43-
The closest signal available right now is the per-crate cold-resolve
44-
criterion bench:
45+
The closest signal available right now without a FastLED checkout is the
46+
per-crate cold and warm criterion benches against `MiniFramework`:
4547

4648
```bash
4749
uv run soldr cargo bench -p fbuild-library-select --bench resolve_cold
50+
uv run soldr cargo bench -p fbuild-library-select --bench resolve_warm
4851
```
4952

50-
That bench drives a synthetic ~30-library Teensyduino-class tree built
53+
Those benches drive a synthetic ~30-library Teensyduino-class tree built
5154
from `fbuild-test-support`'s `MiniFramework` rather than real FastLED
52-
sketches, and it measures the cold path only. It is a useful regression
53-
guard for the resolver itself, but it does **not** satisfy AC#5.
55+
sketches. They are useful regression guards for the resolver and its
56+
cache layer respectively, but they do **not** satisfy AC#5 on their own.
5457

5558
## Cross-links
5659

5760
- Issue: `FastLED/fbuild#205`
58-
- Phase 4 design note (the prerequisite for this directory):
61+
- Phase 4 K/V memoization (shipped in #212):
5962
[`../../tasks/zccache-kv-design.md`](../../tasks/zccache-kv-design.md)
6063
- Subsystem architecture:
6164
[`../../docs/architecture/library-selection.md`](../../docs/architecture/library-selection.md)
6265
- Foundation baseline that the warm threshold compares against:
6366
[`../../tasks/baseline-205.md`](../../tasks/baseline-205.md)
64-
- Per-crate cold benches (different scope, same subsystem):
67+
- Per-crate cold + warm benches (different scope, same subsystem):
6568
[`../../crates/fbuild-library-select/benches/README.md`](../../crates/fbuild-library-select/benches/README.md),
6669
[`../../crates/fbuild-header-scan/benches/README.md`](../../crates/fbuild-header-scan/benches/README.md)

crates/fbuild-library-select/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ fbuild-test-support = { path = "../fbuild-test-support" }
2424
[[bench]]
2525
name = "resolve_cold"
2626
harness = false
27+
28+
[[bench]]
29+
name = "resolve_warm"
30+
harness = false

crates/fbuild-library-select/benches/README.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ End-to-end cold-path measurement of `resolve()` against a synthetic
99
5-deep transitive include chain forces the two-pass LDF reconciliation; the
1010
remaining libraries are unreferenced and must be rejected — that doubles as a
1111
guard against the #204 over-selection regression. Walks the tempdir on every
12-
iteration since no cache sits in front of `resolve()` today (Phase 4
13-
memoization waits on zccache#130).
12+
iteration; the bench calls `resolve()` directly, so no cache layer ever sits
13+
in front of it.
1414

1515
The Phase 7 P-02 threshold from FastLED/fbuild#205 is **≤ 200 ms cold for a
1616
typical teensy41 project**. This bench captures the baseline; future PRs gate
@@ -21,3 +21,26 @@ Run:
2121
```bash
2222
uv run soldr cargo bench -p fbuild-library-select --bench resolve_cold
2323
```
24+
25+
## resolve_warm
26+
27+
Warm cache-hit path through `resolve_cached()` against the same synthetic
28+
~30-library `MiniFramework` tree. The bench builds the fixture once, opens a
29+
`KvStore` in a tempdir, primes the cache with one untimed `resolve_cached`
30+
call (which misses), then times only the second invocation. Each iteration
31+
asserts `from_cache == true` and panics otherwise — that way we can never
32+
silently regress to measuring miss work.
33+
34+
This is the Phase 7 / #215 P-01-mini bench. The real per-board, per-example
35+
warm matrix lives at `bench/fastled-examples/` and depends on a checked-out
36+
FastLED tree; the synthetic mini bench here is the cache-hit regression
37+
guard that runs on every CI of this crate.
38+
39+
Run:
40+
41+
```bash
42+
uv run soldr cargo bench -p fbuild-library-select --bench resolve_warm
43+
```
44+
45+
Compare against `resolve_cold`: warm should be orders of magnitude faster
46+
(K/V lookup + bincode decode vs. full filesystem walk + LDF reconciliation).
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//! P-01 (mini) warm-resolver benchmark.
2+
//!
3+
//! Phase 4 of <https://github.com/FastLED/fbuild/issues/205> shipped
4+
//! [`fbuild_library_select::cache::resolve_cached`], a `KvStore`-backed memo
5+
//! in front of [`fbuild_library_select::resolve`]. AC#5 / P-01 of #205 sets
6+
//! a "warm library-selection ≤ current fbuild + 50 ms" goal; the real
7+
//! per-board matrix lives under `bench/fastled-examples/` and waits on a
8+
//! checked-out FastLED tree. This bench is the per-crate counterpart: it
9+
//! measures the synthetic warm path against the same `MiniFramework`
10+
//! fixture as `resolve_cold`, so we have a regression guard for the cache-
11+
//! hit code path independent of the larger matrix.
12+
//!
13+
//! Structure mirrors `resolve_cold.rs`: build the ~30-library tree once,
14+
//! open a `KvStore` once, prime the cache with one untimed `resolve_cached`
15+
//! call, then time only the second invocation (the hit path).
16+
//!
17+
//! Run with:
18+
//!
19+
//! ```text
20+
//! uv run soldr cargo bench -p fbuild-library-select --bench resolve_warm
21+
//! ```
22+
//!
23+
//! Follows up #215 (mini bench) and #205 Phase 7 (perf budgets).
24+
25+
use std::path::PathBuf;
26+
27+
use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput};
28+
use fbuild_library_select::cache::{resolve_cached, CacheKeyInputs};
29+
use fbuild_packages::library::framework_library::discover_framework_libraries;
30+
use fbuild_packages::library::FrameworkLibrary;
31+
use fbuild_test_support::MiniFramework;
32+
use zccache_artifact::KvStore;
33+
34+
const LIB_COUNT: usize = 30;
35+
const CHAIN_LEN: usize = 5;
36+
37+
fn build_fixture() -> (
38+
MiniFramework,
39+
Vec<PathBuf>,
40+
Vec<PathBuf>,
41+
Vec<FrameworkLibrary>,
42+
) {
43+
let mut mf = MiniFramework::new();
44+
for i in 0..LIB_COUNT {
45+
let name = format!("Lib{i:02}");
46+
let next = if i + 1 < CHAIN_LEN {
47+
Some(format!("Lib{:02}", i + 1))
48+
} else {
49+
None
50+
};
51+
let header = if let Some(n) = &next {
52+
format!("#pragma once\n#include <{n}.h>\n")
53+
} else {
54+
"#pragma once\n".to_string()
55+
};
56+
let cpp = format!("#include <{name}.h>\nvoid {name}_func() {{}}\n");
57+
mf.add_library(&name).header(&header).cpp(&cpp).done();
58+
}
59+
mf.sketch("#include <Lib00.h>\nvoid setup() {}\nvoid loop() {}\n");
60+
61+
let libs = discover_framework_libraries(&mf.libraries_dir());
62+
let seeds = mf.project_seeds();
63+
let search_paths = mf.project_search_paths();
64+
(mf, seeds, search_paths, libs)
65+
}
66+
67+
fn bench_resolve_warm(c: &mut Criterion) {
68+
let (mf, seeds, search_paths, libs) = build_fixture();
69+
let kv_dir = tempfile::tempdir().expect("resolve_warm: failed to create kv tempdir");
70+
let kv = KvStore::open(kv_dir.path().join("kv")).expect("resolve_warm: KvStore::open failed");
71+
72+
let framework_root = mf.framework_root().to_path_buf();
73+
let inputs = CacheKeyInputs {
74+
toolchain_triple: "avr-unknown-none",
75+
framework_install_path: &framework_root,
76+
framework_version: "1.59.0",
77+
};
78+
79+
// Prime the cache so the timed loop measures the hit path only.
80+
let primed = resolve_cached(&seeds, &search_paths, &libs, &inputs, &kv)
81+
.expect("resolve_warm: prime resolve_cached failed");
82+
assert!(
83+
!primed.from_cache,
84+
"resolve_warm: priming call must miss (got hit)"
85+
);
86+
87+
// WHY: confirm the very next call hits before entering the bench loop;
88+
// otherwise we'd silently measure cold work and report a misleading
89+
// baseline.
90+
let probe = resolve_cached(&seeds, &search_paths, &libs, &inputs, &kv)
91+
.expect("resolve_warm: probe resolve_cached failed");
92+
assert!(
93+
probe.from_cache,
94+
"resolve_warm: second call did not hit cache; bench would measure misses"
95+
);
96+
97+
let mut group = c.benchmark_group("resolve");
98+
group.throughput(Throughput::Elements(libs.len() as u64));
99+
group.bench_function("warm_30_libs_chain_5", |b| {
100+
b.iter(|| {
101+
let res = resolve_cached(
102+
black_box(&seeds),
103+
black_box(&search_paths),
104+
black_box(&libs),
105+
black_box(&inputs),
106+
black_box(&kv),
107+
)
108+
.expect("resolve_warm: resolve_cached failed inside bench loop");
109+
if !res.from_cache {
110+
panic!("resolve_warm: bench iteration missed the cache");
111+
}
112+
black_box(res);
113+
});
114+
});
115+
group.finish();
116+
drop(mf);
117+
drop(kv_dir);
118+
}
119+
120+
criterion_group!(benches, bench_resolve_warm);
121+
criterion_main!(benches);

0 commit comments

Comments
 (0)