Skip to content

Commit ea420d5

Browse files
committed
Encode codegen incrementality benchmark evidence (#25)
Scratch-workspace matrix (warm Bazel 9.2.0 server, plan aspect + dx_codegen_plans): source/BUILD-edit and target add/remove medians over 3 reps, executed-action counts, 238-byte plan materialization, plan-vs-default projection walls, and server peak RSS. Query-file control matches baseline on every row; freeze unchanged.
1 parent ea9c553 commit ea420d5

2 files changed

Lines changed: 141 additions & 16 deletions

File tree

‎docs/environments/codegen.md‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@ With no argument, `dx codegen` selects every codegen projection in the currently
3737
declared Bazel BUILD graph and atomically selects one repository-wide generated-source
3838
projection. It does not inspect unowned files or decide that BUILD metadata ought to
3939
exist. The physical root-selection mechanism is frozen to the `//...` baseline
40-
(see `FROZEN_STRATEGY` in `dx/roots/src/lib.rs`); only cold/warm behavior is measured,
41-
and the remaining incrementality dimensions are tracked in
42-
[#25](https://github.com/ralvik/rules_dx/issues/25).
40+
(see `FROZEN_STRATEGY` in `dx/roots/src/lib.rs`); cold/warm behavior and the
41+
incrementality dimensions (source/BUILD edits, target add/remove, actions,
42+
materialized bytes, projection time, retained memory) are measured in
43+
`FROZEN_EVIDENCE` and `INCREMENTALITY_EVIDENCE` there. Concurrency,
44+
interruption, remote materialization, and reuse certification stay open per
45+
that crate's docs.
4346

4447
Repository-wide selection includes every registered production, test, example, and
4548
development projection. It does not omit test-only generators to improve performance.
@@ -194,8 +197,10 @@ uncached generator actions. A query-produced target-pattern file is expected to
194197
target-pattern discovery and unrelated loading compared with `//...`, but it still loads
195198
packages containing indexed labels and analyzes every selected configured closure.
196199
Bazel's persistent server and action cache are expected to improve unchanged warm runs;
197-
the required evidence is tracked in
198-
[#25](https://github.com/ralvik/rules_dx/issues/25) rather than a verified claim.
200+
the measured evidence lives in `FROZEN_EVIDENCE` and `INCREMENTALITY_EVIDENCE`
201+
in `dx/roots/src/lib.rs`. Concurrency, interruption, remote materialization,
202+
and reuse certification remain unverified claims tracked as later WP4 slices
203+
in that crate.
199204

200205
Exact-target runs bypass repository root selection and analyze only one configured
201206
target closure. Generator action invalidation may be narrow after one schema change,

‎dx/roots/src/lib.rs‎

Lines changed: 131 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121
//! invocations (`repository_plan`, `invocation_targets`, `build_argv`);
2222
//! the query/aggregate Starlark wiring lives in `//dx/roots:roots.bzl`.
2323
//! Slice 3 freezes the measured cold/warm winner (the `//...` baseline;
24-
//! see [`frozen_strategy`] and [`FROZEN_EVIDENCE`]). The remaining
25-
//! benchmark dimensions (source/BUILD edits, target churn, actions,
26-
//! materialized bytes, projection time, retained memory) plus concurrency,
27-
//! interruption, remote materialization, and reuse certification land in
28-
//! later WP4 slices; the effective roots stay on the frozen baseline until
29-
//! then.
24+
//! see [`frozen_strategy`] and [`FROZEN_EVIDENCE`]). Slice 4 lands the
25+
//! incrementality dimensions for the frozen baseline (source/BUILD edits,
26+
//! target churn, actions, materialized bytes, projection time, retained
27+
//! memory; see [`INCREMENTALITY_EVIDENCE`] and the `PLAN_*`/`SERVER_*`
28+
//! consts). Concurrency, interruption, remote materialization, and reuse
29+
//! certification land in later WP4 slices; the effective roots stay on the
30+
//! frozen baseline until then.
3031
3132
use std::collections::BTreeSet;
3233
use std::path::{Path, PathBuf};
@@ -77,11 +78,12 @@ pub const WARM_WEIGHT: u64 = 2;
7778
/// Weighted scores (`cold_ms + WARM_WEIGHT * warm_ms`, warm medians 372
7879
/// vs 350): baseline 8457 + 2*372 = 9201 beats query-file 8983 + 2*350 =
7980
/// 9683 outright; the baseline also wins every tie by [`RootStrategy::ALL`]
80-
/// order. The remaining [`BenchmarkDimension`] rows (source/BUILD edits,
81-
/// target churn, actions, materialized bytes, projection time, retained
82-
/// memory) plus concurrency, interruption, remote materialization, and
83-
/// reuse certification land in later WP4 slices and cannot displace this
84-
/// freeze without new measured evidence plus a freeze change here.
81+
/// order. The incrementality rows now land in [`INCREMENTALITY_EVIDENCE`]
82+
/// (#25 slice) and confirm the freeze: the query-file control matches the
83+
/// baseline on every dimension within noise. Concurrency, interruption,
84+
/// remote materialization, and reuse certification land in later WP4 slices
85+
/// and no row here can displace this freeze without new measured evidence
86+
/// plus a freeze change here.
8587
pub const FROZEN_STRATEGY: RootStrategy = RootStrategy::RecursivePattern;
8688

8789
/// Returns the frozen repository-root strategy (see [`FROZEN_STRATEGY`]).
@@ -102,6 +104,77 @@ pub const FROZEN_EVIDENCE: [(RootStrategy, bool, u64, u64); 4] = [
102104
(RootStrategy::PackageShards, false, 2000, 300),
103105
];
104106

107+
/// Measured incrementality evidence for the frozen baseline (M25 WP4 slice
108+
/// 4, issue #25): steady-state warm-server wall times and executed actions
109+
/// per edit/churn [`BenchmarkDimension`], as `(dimension,
110+
/// baseline_wall_ms, queryfile_wall_ms, actions_executed)`.
111+
///
112+
/// Methodology (2026-09-15, Linux x86_64, Bazel 9.2.0 via Bazelisk
113+
/// v1.29.0): scratch copy of the workspace (rsync, `.git`/`bazel-*`
114+
/// excluded) with its own warm persistent server, so the live repo is
115+
/// never dirtied. Every cell applies one probe edit, builds the codegen
116+
/// plan aspect (`//generation:codegen.bzl%dx_codegen_plan_aspect`) with the
117+
/// `dx_codegen_plans` output group, records `Elapsed time` plus the
118+
/// executed-action summary, then reverts to pristine. Probes are
119+
/// comment-only (source: `//` line on `generation/codegen.proto`; BUILD:
120+
/// `#` line on `generation/BUILD.bazel`) except churn, which adds/removes
121+
/// a trivial `filegroup`; each rep carries a unique comment tag so no rep
122+
/// action-cache-hits a previous one, and a throwaway warm-up probe per
123+
/// dimension absorbs the first-probe package-reload artifact (see below).
124+
/// Reported walls are medians over 3 measured reps. The query-file
125+
/// candidate reads the `roots_pattern_fixture` shape (holding `//...`)
126+
/// via `--target_pattern_file`, so it is an equivalent-semantics control:
127+
/// it matches the baseline on every row within noise and cannot displace
128+
/// the freeze.
129+
///
130+
/// Findings pinned below:
131+
/// - Source edits re-execute exactly 2 actions (`GenProtoDescriptorSet`
132+
/// and `ProstGenProto` on `//generation:codegen_proto`). Comment-only
133+
/// probes leave outputs byte-identical (md5-verified, 0 bytes
134+
/// rewritten), so these walls are a LOWER BOUND: semantic edits cost at
135+
/// least this plus downstream propagation.
136+
/// - BUILD edits cost re-analysis only at steady state (0 actions). The
137+
/// discarded warm-up probe deterministically re-executed the same 2
138+
/// proto actions once (package reload coinciding with a restored source
139+
/// mtime); reps 1-3 with unique comments execute nothing.
140+
/// - Target add/remove cost re-analysis only (0 actions); remove is
141+
/// marginally slower than add.
142+
/// - `TargetAddRemove` rows keep the slower of the add/remove medians
143+
/// (add: 524/542 ms, remove: 531/535 ms baseline/query-file).
144+
pub const INCREMENTALITY_EVIDENCE: [(BenchmarkDimension, u64, u64, u64); 3] = [
145+
(BenchmarkDimension::SourceEdit, 446, 471, 2),
146+
(BenchmarkDimension::BuildEdit, 442, 469, 0),
147+
(BenchmarkDimension::TargetAddRemove, 531, 542, 0),
148+
];
149+
150+
/// Files materialized in `bazel-bin` for the `dx_codegen_plans` output
151+
/// group over `//...`: 3 `.dxcodegen.pb` shards (see [`PLAN_MATERIALIZED_BYTES`]).
152+
pub const PLAN_MATERIALIZED_FILES: u64 = 3;
153+
154+
/// Bytes materialized for the plan output group: the 3 shards total 238
155+
/// bytes. Per-probe re-materialization is 0 bytes at steady state: edit
156+
/// probes reproduce byte-identical outputs, which Bazel leaves in place.
157+
pub const PLAN_MATERIALIZED_BYTES: u64 = 238;
158+
159+
/// Warm wall time (median ms) materializing only the plan output group:
160+
/// the Bazel-layer projection cost behind `dx codegen` selection.
161+
pub const PLAN_GROUP_WARM_MS: u64 = 413;
162+
163+
/// Warm wall time (median ms) building `//...` default outputs with the
164+
/// aspect applied but no output group requested. Excludes the one-time
165+
/// 130543 ms first build of never-built default outputs in the scratch
166+
/// workspace; the plan group (`PLAN_GROUP_WARM_MS`) stays cheaper than
167+
/// full default outputs. CLI-layer link-tree projection timing is not
168+
/// implemented yet and stays open.
169+
pub const DEFAULT_OUTPUTS_WARM_MS: u64 = 502;
170+
171+
/// Bazel server peak resident set (VmHWM KiB) after the full benchmark
172+
/// matrix plus one full default-outputs build: ~2.5 GiB retained for the
173+
/// `//...` analysis graph. An upper bound for plan-only iteration, which
174+
/// never approaches it (warm plan builds sit near 0.4 s with 0 executed
175+
/// actions).
176+
pub const SERVER_PEAK_RSS_KB: u64 = 2628812;
177+
105178
/// Repository-root strategy candidates under O34.
106179
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
107180
pub enum RootStrategy {
@@ -673,6 +746,53 @@ mod tests {
673746
assert_eq!(select_strategy(&samples), frozen_strategy());
674747
}
675748

749+
#[test]
750+
fn incrementality_evidence_covers_the_measured_dimensions_once() {
751+
let dims: Vec<BenchmarkDimension> =
752+
INCREMENTALITY_EVIDENCE.iter().map(|row| row.0).collect();
753+
assert_eq!(
754+
dims,
755+
vec![
756+
BenchmarkDimension::SourceEdit,
757+
BenchmarkDimension::BuildEdit,
758+
BenchmarkDimension::TargetAddRemove,
759+
]
760+
);
761+
}
762+
763+
#[test]
764+
fn incrementality_control_matches_baseline_within_noise() {
765+
for (dimension, baseline_ms, queryfile_ms, actions) in INCREMENTALITY_EVIDENCE {
766+
let slower = baseline_ms.max(queryfile_ms);
767+
let faster = baseline_ms.min(queryfile_ms);
768+
assert!(
769+
slower * 100 <= faster * 120,
770+
"{dimension:?}: baseline {baseline_ms} ms vs query-file {queryfile_ms} ms diverge past 20%"
771+
);
772+
assert!(
773+
actions <= 2,
774+
"{dimension:?}: {actions} executed actions exceed the measured maximum"
775+
);
776+
}
777+
assert_eq!(
778+
INCREMENTALITY_EVIDENCE[0].3, 2,
779+
"source edits re-execute 2 actions"
780+
);
781+
}
782+
783+
#[test]
784+
fn plan_materialization_stays_cheaper_than_default_outputs() {
785+
assert_eq!(PLAN_MATERIALIZED_FILES, 3);
786+
assert_eq!(PLAN_MATERIALIZED_BYTES, 238);
787+
assert!(
788+
PLAN_GROUP_WARM_MS < DEFAULT_OUTPUTS_WARM_MS,
789+
"plan group {} ms must stay cheaper than default outputs {} ms",
790+
PLAN_GROUP_WARM_MS,
791+
DEFAULT_OUTPUTS_WARM_MS
792+
);
793+
assert!(SERVER_PEAK_RSS_KB > 0);
794+
}
795+
676796
#[test]
677797
fn baseline_plan_keeps_the_canonical_selection_identity() {
678798
let plan = repository_plan();

0 commit comments

Comments
 (0)