cargo perf --repeat N: report a median and a spread, not a point sample #295
Labels
No labels
agent
agent:ci
agent:done
agent:failed
agent:needs-input
agent:refined
agent:refining
agent:running
agent:shipped
agent:skip
autonomous
autopilot
driven
local
plan
proposal
qa
qa-gap
research
retro
ship
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jeroen/cartopolis#295
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
cargo perfmeasures each scenario once and reports that single reading as the value.perf::Window(crates/cartopolis/src/systems/dev/perf.rs:297-347) has onef64/u64per column,Report::windowsis a flatVec<Window>(perf.rs:366-370), and the harness flies each scenario exactly once —ShotProgress::indexadvances past every shot and never revisits one (crates/cartopolis/src/systems/dev/shot_harness.rs:2497-2508).That is the shape of every result this suite has had to withdraw:
docs/notes/render-performance-survey.md:346-349— "repeats of one config spanned 218–319 ms … The within-group spread exceeds every between-group difference." Three configurations were compared on medians the author had to compute by hand from three separate launches.docs/notes/performance-suite.md:141-149lists three withdrawn findings, each one sample differenced against another sample.docs/notes/performance-suite.md:158— "identical builds have been measured 25 % apart."The module already knows this and already fixed it inside one window:
GpuPass::frag_min/frag_max/frag_samplessample the fill counter every frame precisely because "a point sample of this is not reproducible and will mislead you" (perf.rs:283-293, sampled atperf.rs:573-581). Nothing does the same across windows, so every column that is notfragment_invocationsis still a point sample, and the only way to get a spread today is to launchcargo perfN times and diff N JSON files by hand.There is no
--repeatanywhere in the client (grep repeatoverperf.rs,shot_harness.rs,lib.rsreturns only an unrelated comment atlib.rs:2688).Nothing outside the crate reads
perf.json— no test, no CI job, no tool (grep -rn "perf.json\|perf::Report"finds only docs andlib.rs:275). The report shape can change freely.Approach
Two files, plus documentation.
1.
crates/cartopolis/src/lib.rs— the flag#[arg(long, default_value_t = 1, value_name = "N")] repeat: u32beside the existing--perf-out/--perf-frames(lib.rs:113-120).--repeat 0is an error, not a clamp — same rule the manifest already applies to a typo'd--onlyname (perf.rs:158-162).--repeatwith a value other than 1 and no--perfis an error, following the precedent atlib.rs:299-301where--onlyoutside a--shotsrun is refused rather than ignored.systems::perf::buildatlib.rs:268-277.2.
crates/cartopolis/src/systems/dev/perf.rsFlying the repeats.
build(perf.rs:444-525) already turns the selected scenarios intoShotHarness::shotsand a parallelPerfRun { names, steady_frames }. Repeat all three listsrepeattimes round-robin (a b c a b c a b c), not blocked (a a a b b b). Reason: the drift this ticket exists to expose is the shared box, and blocked repeats put all three samples of one scenario adjacent in time, so a slow patch lands entirely inside one scenario's group and is attributed to the scene. Round-robin spreads it over all of them. Everything downstream already works per shot index:place_camerare-issues the teleport for whateverprogress.indexnames (shot_harness.rs:2059-2072), anddrive_perf_windowreadsprogress.shot_index()to label the window (perf.rs:559,perf.rs:670-675). The end-of-run testperf.index + 1 >= run.names.len()(perf.rs:620) still fires on the last flight.Add
repeat: u32toPerfRun(perf.rs:195-203) sofinishcan record it.PNG filenames.
ShotSpec::outisperf-{name}.png(perf.rs:473) and repeats would collide on one file. Whenrepeat == 1keep that name byte-for-byte; otherwise writeperf-{name}-r{n}.png, 1-based. A picture per repeat is what explains a spread that turns out to be "a tile had not arrived that time" — the same reasonperf.rs:470-472gives for writing a PNG at all.The report. Keep
Window(perf.rs:297-347) exactly as it is — it is whatclosebuilds and whatlog_windowprints per flight (perf.rs:718,perf.rs:723-769), so the raw per-run numbers stay in the log. Change whatfinish(perf.rs:772-806) serialises:ReportMeta(perf.rs:350-364) gainspub repeat: u32.The aggregation is one pure function —
fn summarise(samples: &[Window]) -> Vec<WindowSummary>— takingPerfProgress::samplesand grouping by(scenario, phase). Being pure and taking a slice is what makes it testable with no app (the whole of the rest of this module needs aWorld). Rules, all pinned by tests:(scenario, phase); output order is first appearance, i.e. manifest order withloadbeforesteadyfor each scenario, so the JSON reads the same as a--repeat 1report does today.WindowSummary::runsis the number of flights in the group.Stat::runsis the number of those flights that carried a value for that column — anOption<f64>column absent in some runs aggregates over the present ones and says so, rather than counting a missing diagnostic as zero (theperf.rs:26-27rule: "where they are missing the columns come back absent rather than zero"). A column absent in every run isNone.perf windowlog lines gets the same number the JSON reports — a suite whose arithmetic disagrees with the reader's is another way to withdraw a result. The alternative (lower middle, so every reported number is one actually measured) is named here so nobody has to re-derive the choice;--repeat 3never reaches the difference.n == 1givesmin == median == maxby construction, not by a special case.gpu_passesmerge bypath.frag_min= min of the per-runfrag_mins,frag_max= max of the per-runfrag_maxes,frag_samples= sum.fragment_invocations,gpu_ms,cpu_msbecomeStats. Sort descending by medianfragment_invocations, matching the existing sort atperf.rs:425-429, with passes carrying no statistics last.One summary log line per aggregated window, written from
finishalongside the existingperf: wrote reportline, in the shapelog_windowalready uses (perf.rs:723-769) — scenario, phase, runs, quiet_runs, andmedian (min..max)forframe_time_msandfps. Same reason that function gives: a shell that never opens the JSON still learns something.3. Documentation
perf.rsmodule doc "Running it" (perf.rs:49-55) — addcargo perf --repeat 3 --only orbit.docs/perf.tomlheader (lines 3-5) and thecargo perfcomment block in.cargo/config.toml:83-88— same line.docs/notes/performance-suite.md— this is a standing fact, so it goes in the note (value 7, Say what you decided). Under "A point sample of the fill counter is not reproducible" (performance-suite.md:71-81), record that the same argument applies to every other column across launches, that--repeat Nis the answer, the even-n median rule, and the caveat below.The caveat that must be written down: a repeat's
loadwindow is not a cold one. Tiles are cached inplatform::storage::Namespace::Cacheon disk and inTileCachein memory (crates/cartopolis/src/systems/map/tile_loader.rs:143-146), so repeat 2 of a scenario re-streams from a warm process.loadmedians therefore describe a re-teleport, not a cold start, and repeat 1 is not comparable with repeats 2..N. (The disk half of this is already true today between launches; the in-memory half is new with--repeat.) Thesteadywindow is unaffected — it is what this flag is for.Acceptance criteria
--repeat Nexists on the CLI, defaults to 1, and is documented in its clap doc comment.--repeat 0fails at startup with a message naming the flag;--repeatother than 1 without--perffails at startup, matchinglib.rs:299-301.cargo perf --repeat 3 --only orbitfliesorbitthree times in one launch and writes one report.windowshas exactly two entries (orbit/load,orbit/steady), each withruns: 3, and each numeric column an object withruns,min,median,max.meta.repeatrecords the repeat count.cargo perf --only orbit(no flag) still produces two windows withruns: 1andmin == median == maxon every column, and still writesperf-orbit.pngunder that exact name.repeat > 1, each flight writes its ownperf-{name}-r{n}.png.finishlogs one summary line per aggregated window carrying runs, quiet_runs and the median plus range forframe_time_msandfps; the existing per-flightperf windowlines are unchanged.Windowvalues, all inperf.rs's existing#[cfg(test)] mod tests(perf.rs:819):(scenario, phase)→ one summary,runs == 3, median is the middle value, min/max correct;min == median == max,runs == 1;Stat::runs == 2and the aggregate is over those two; a columnNonein all runs → the field isNone;gpu_passesmerge by path:frag_minis the min of the mins,frag_maxthe max of the maxes,frag_samplesthe sum, and a pass present in only some runs reports the smallerruns.docs/notes/performance-suite.md,docs/perf.toml,.cargo/config.tomland theperf.rsmodule doc all mention--repeat, and the note carries the warm-loadcaveat.Verification
Runs here, in this container (lavapipe;
orbitis the one scenario cheap enough —docs/perf.toml:91-95says a city view takes minutes to settle):Then read
/tmp/perf-r3.json: two windows,runs: 3, three distinct samples visible asmin != maxonframe_time_ms, andmeta.repeat == 3. Read/tmp/perf-r1.json:runs: 1andmin == median == maxthroughout. Cross-check the medians against the threeperf windowlog lines the run printed.The timing columns in either report are properties of a CPU rasteriser on a shared box and are not evidence of anything about the client (
docs/notes/performance-suite.md:155-159) — they are being read here only to confirm the aggregation ran, not to conclude anything.Only a workstation or the Steam Deck runner can check that the spread the flag now reports is small enough to make a between-config difference readable — the question
docs/notes/render-performance-survey.md:344-352could not answer. That is a measurement, so it is not this ticket's; it is what the next A/B uses the flag for. Nothing in this pass was compiled: this was a read-only refinement.Out of scope
windowscarries the aggregate only. Every individual flight is already printed bylog_window(perf.rs:718), so nothing is lost; adding asamplesarray is a follow-up if a reader ever wants n > 5 raw.--expect-style assertion over a perf report. There is no CI job runningcargo perfat all today.repeatkey indocs/perf.toml. CLI only; the manifest'sdeny_unknown_fields(perf.rs:83) will reject one, which is the correct answer.loadcold between repeats (cache eviction, a fresh process per repeat). Documented as a caveat, not solved.docs/notes/performance-suite.md:161-163).docs/perf.toml. An entry there is a claim the viewpoint has been measured (docs/perf.toml:91-95).Open questions
None.
Branch:
feat/295-perf-repeat-median-spreadOriginal request
From the maintainer's focus for this lane:
cargo perf --repeat N: report a median and a spread, not a point sample
Lands in crates/cartopolis/src/systems/dev/perf.rs and the --perf flag block in lib.rs. Every withdrawn result in docs/notes/performance-suite.md was one sample differenced against one other sample, and docs/notes/render-performance-survey.md records a within-group spread wider than every between-group difference it was trying to measure. --repeat N re-flies the selected scenarios inside one launch and each Window in the JSON carries runs, min, median and max for every numeric column instead of a single value. Judged by: a --repeat 3 report whose windows carry three samples and a median, plus a unit test over synthetic windows pinning the aggregation, including that --repeat 1 still reports min == median == max. Needs no Steam Deck to build or test.
This is item 2 of 10 of the lane's queue. It is ordered by a person in the
admin console, so build this one rather than the one you would have picked — and keep it
the size it is. A ticket that turns out to be three tickets is one ticket: finish this
part and say on it what the other two are.
If the item is simply wrong — already done, impossible, or a bad idea against the values
— say so here and close it. That is a result, not a failure.
Decide it yourself. This ticket is not being watched, so a question asked here is a
ticket that stops. The seven values at the top of
docs/direction.mdexist to settleexactly that kind of ambiguity: pick the reading they support, say in the commit body
which one you applied, and build. Only a decision needing something nobody can derive
from the repository — a credential, a licence somebody must accept, a choice about what
the project is for — is a reason to stop.
Filed by the autopilot.
🤖 Refined by the viberfox issue agent. Reply with @agent refine and what is wrong to have this rewritten.
🤖 Refining this before anything gets built — reading the code, no branch, no commits. If it turns out I'd have to guess at the requirements I'll stop and ask instead.
🤖 Refined. The description above is now the spec; I'll pick it up for implementation on the next poll.
If the direction is wrong, say so now — @agent refine with a correction rewrites it, and
agent:skipstops me.🤖 viberfox issue agent on the VPS — 22 turns · $2.54 · session
53840b6a-ee46-4d5a-862a-5c34f525cb33. Reply with @agent to continue this session.🤖 Picked this up — working on branch
feat/295-perf-repeat-median-spreadin a fresh worktree on the VPS. I'll report back here when I'm done.Label an issue
agent:skipto keep me off it, ordrivento keep it in a session you are driving.The session was killed after hitting the 45 minute limit.
No commits were made.
🤖 viberfox issue agent on the VPS — . Reply with @agent to continue this session.
alreadyFiledonly ever sees 50 issues, so every autopilot dedupe key silently expires — #206 was QA'd twice #281Built in the nightly run: PR #306 (
feat/295-perf-repeat), verify-branch green one486d02, and checked end to end withcargo perf --only orbit --repeat 3. Not merged.