T08 complete: benchmarks, determinism evidence, and one missed metric
evidence/CB-EV-0001-game-kernel.md records the acceptance run against the CB-RES-0001 baseline. Met: AM-1 rule coverage 58/58; AM-6 throughput 1.65M events/s against a 100k target; AM-7 scaling 1.08x at 20x workload and a 100k-event replay in 4.13ms against a 5s budget; AM-8 zero divergence over 10 full runs with fmt and clippy clean; AM-10 zero foreign collection types. Not met and reported as such: AM-4 at 33 transitive crates against a <=20 target. Attribution is in the evidence file. The recommended fix is making serde_yaml optional (-5, a test-only concern), after which the remainder is sha2 and rand_chacha, which K5 and K7 require. We are not hand-rolling crypto primitives to win a dependency count. AM-12 is recorded as uncomputable: per-task token counts were never instrumented, and inventing a USD figure would defeat the metric. A measurement error was found and corrected before publication. The first benchmark reported 9.3M events/s on a flat curve. The workload had a player selecting SUPPORT while parked at Stress 4, so GR-R03 rejected it, rounds never completed, and throughput was computed for rounds that never happened. The bench now asserts the per-round event count and panics rather than measuring a stalled loop. The corrected figure is 5.6x lower. The evidence file states plainly what the boardgame.io comparison does and does not support: the ~450x command-rate ratio is cross-runtime and cross-feature-set, so it is a direction, not a verdict, per the InnerLoop parity-cap rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
85d93a9e3c
commit
eb1378e667
4 changed files with 569 additions and 37 deletions
|
|
@ -2087,3 +2087,164 @@ mod tests {
|
|||
assert_ne!(state_hash_hex(&a), state_hash_hex(&c));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod bench_shape {
|
||||
use super::*;
|
||||
use cb_game_runtime::{ScenarioGame, Setup};
|
||||
|
||||
/// AM-6 reports events/second; the evidence file converts that to
|
||||
/// rounds and commands per second. Both divisors are pinned here so
|
||||
/// a change to the workload cannot silently rescale the metric.
|
||||
#[test]
|
||||
fn synthetic_round_shape_is_pinned() {
|
||||
let mut state = GroundState::setup(
|
||||
&Setup {
|
||||
players: 3,
|
||||
preset: "standard-3p".into(),
|
||||
patch: BTreeMap::new(),
|
||||
},
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut events = 0;
|
||||
let mut commands = 0;
|
||||
let mut run = |state: &mut GroundState, actor: Actor, cmd: &GroundCommand| {
|
||||
commands += 1;
|
||||
if let Ok(produced) = state.validate(actor, cmd) {
|
||||
for e in &produced {
|
||||
state.fold(e);
|
||||
}
|
||||
events += produced.len();
|
||||
}
|
||||
};
|
||||
|
||||
for (seat, action, target) in [
|
||||
(0u8, Action::Attack, Some(PlayerId(1))),
|
||||
(2, Action::Support, Some(PlayerId(1))),
|
||||
(1, Action::Ground, None),
|
||||
] {
|
||||
run(
|
||||
&mut state,
|
||||
Actor::Player(PlayerId(seat)),
|
||||
&GroundCommand::SelectAction {
|
||||
action,
|
||||
target,
|
||||
problem: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
run(&mut state, Actor::System, &GroundCommand::Reveal);
|
||||
run(
|
||||
&mut state,
|
||||
Actor::Player(PlayerId(1)),
|
||||
&GroundCommand::ChooseGroundMode {
|
||||
mode: GroundMode::Gr,
|
||||
choice: None,
|
||||
},
|
||||
);
|
||||
run(&mut state, Actor::System, &GroundCommand::Resolve);
|
||||
run(&mut state, Actor::System, &GroundCommand::EndRound);
|
||||
|
||||
assert_eq!(commands, 7, "commands per synthetic round");
|
||||
assert_eq!(events, 13, "events per synthetic round");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod replay_probe {
|
||||
use super::*;
|
||||
use cb_events::state_hash_hex;
|
||||
use cb_game_runtime::{ScenarioGame, Setup};
|
||||
use std::time::Instant;
|
||||
|
||||
fn fresh(seed: u64) -> GroundState {
|
||||
GroundState::setup(
|
||||
&Setup {
|
||||
players: 3,
|
||||
preset: "standard-3p".into(),
|
||||
patch: BTreeMap::new(),
|
||||
},
|
||||
seed,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn record_round(state: &mut GroundState, log: &mut Vec<GroundEvent>) -> usize {
|
||||
let mut n = 0;
|
||||
let mut run = |state: &mut GroundState, actor: Actor, cmd: &GroundCommand| {
|
||||
if let Ok(produced) = state.validate(actor, cmd) {
|
||||
for e in &produced {
|
||||
state.fold(e);
|
||||
log.push(e.clone());
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
for (seat, action, target) in [
|
||||
(0u8, Action::Attack, Some(PlayerId(1))),
|
||||
(2, Action::Support, Some(PlayerId(1))),
|
||||
(1, Action::Ground, None),
|
||||
] {
|
||||
run(
|
||||
state,
|
||||
Actor::Player(PlayerId(seat)),
|
||||
&GroundCommand::SelectAction {
|
||||
action,
|
||||
target,
|
||||
problem: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
run(state, Actor::System, &GroundCommand::Reveal);
|
||||
run(
|
||||
state,
|
||||
Actor::Player(PlayerId(1)),
|
||||
&GroundCommand::ChooseGroundMode {
|
||||
mode: GroundMode::Gr,
|
||||
choice: None,
|
||||
},
|
||||
);
|
||||
run(state, Actor::System, &GroundCommand::Resolve);
|
||||
run(state, Actor::System, &GroundCommand::EndRound);
|
||||
n
|
||||
}
|
||||
|
||||
/// AM-7: folding a 100k-event log back into state must stay well
|
||||
/// under the 5s budget, and must be linear in log length.
|
||||
#[test]
|
||||
fn replay_100k_events_is_linear_and_fast() {
|
||||
for target in [10_000usize, 100_000] {
|
||||
let mut log = Vec::with_capacity(target);
|
||||
let mut source = fresh(42);
|
||||
let mut games = 0u64;
|
||||
let mut stalls = 0;
|
||||
while log.len() < target {
|
||||
if source.outcome.is_some() {
|
||||
games += 1;
|
||||
source = fresh(42 + games);
|
||||
}
|
||||
if record_round(&mut source, &mut log) == 0 {
|
||||
stalls += 1;
|
||||
assert!(stalls < 10, "round produced no events; builder stalled");
|
||||
}
|
||||
}
|
||||
let start = Instant::now();
|
||||
let mut state = fresh(42);
|
||||
for event in &log {
|
||||
state.fold(event);
|
||||
}
|
||||
let hash = state_hash_hex(&state);
|
||||
let elapsed = start.elapsed();
|
||||
println!(
|
||||
"replay {} events in {:?} ({:.0} events/s), hash {}",
|
||||
log.len(),
|
||||
elapsed,
|
||||
log.len() as f64 / elapsed.as_secs_f64(),
|
||||
&hash[..8]
|
||||
);
|
||||
assert!(elapsed.as_secs_f64() < 5.0, "AM-7: 100k replay under 5s");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue