- One common set per content profile, shared by every match and the loadout menu; a restart over the same map keeps its images and static geometry. - Weapons resolve as families: attachments are chosen from each game's own tables, checked, and composed into prepared configurations with their first-person skeleton assembled once. - Sessions end through one teardown and one process exit; retired resources are dropped on a worker, not the frame. - Sound is captured by the walks that already read each zone. - A loading screen that reports what the load is doing. - Tests live only in crates/approved_tests (heavy_gameplay_lifecycle); publish-check refuses them anywhere else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.6 KiB
make bench — three reports, one run, one package
IW4L_BENCH=1 turns on an in-process recorder. At exit it prints three
independent reports to iw4l-artifacts/bench/<stamp>.txt. None needs a trace
file; any can be MISS while the others stand. Benchmarks keep running when
unfocused; the window runner does not impose its background 60 Hz sleep.
make bench demo0011 # play a demo (a goal is always a demo name)
make bench ZONE=mp_boneyard # a live map instead
IW4L_BENCH_JOBS=assets make bench demo0011 # a load_jobs row per asset
IW4L_BENCH_FRAMES=0 make bench demo0011 # no per-frame table
[1/3] Map load — the waterfall from the shell command to a playable map, the
load-pool stages, then each image plan's wait split from its decode. A stage's
own duration says nothing on a parallel walk, so the table carries sole_open:
how much of the window that stage was the only thing running. It is not a
bound on what deleting it would save. A stage's RSS delta is the whole process,
so the deltas do not add up. Match audio is split by decoder, in worker time.
[2/3] Frame time — every typed span from PERF.md as a tree, for
the frames after the first playable one, then the rows: first seconds against
steady state (exact percentiles), the worst frames, and unclassified — wall
minus the union of the frame's declared top-level spans, so two threads
inside one nanosecond cover it once and the remainder is never clamped. A span
is charged by intersection: a task spanning four frames shows its part in each
and the rest as carried in. The frame clock closes and reopens at the top of
First, before anything else runs, so every main-phase interval falls wholly
inside one frame; the frame the process exits from is closed at exit and its
row carries partial. Which spans are roots is declared, not read off a
thread's stack — an executor is free to run a schedule's begin and its end
on different workers, and the report counts those as migrated. Nesting is
still observed, and the report counts frames where a span outlives its parent.
main/render overlap is the part of the wall a main schedule and the render
thread were both inside, from their intervals. It is zero whenever rendering is
serialised (IW4L_PIPELINED_RENDERING=0). By default the render world runs
one frame behind on its own thread. Extraction remains a synchronised boundary;
scheduling in the manifest records the mode. Compare completed render cadence
and presented_state_age alongside main-loop wall time.
[3/3] Counters — render stages, the bodies inside Present and Ui, the
HUD schedule gaps, GPU passes, per-frame work. A *_schedule_interval is a gap
between two systems, which the executor is free to fill: compare it against
hud_tess_body, never add the two. A counter nobody sampled prints MISS, never
0.00; GPU passes overlap and resolve late, so they are never summed.
The run's own directory, beside the trace.pftrace:
report.txt— the three reports above;manifest.json— what the run was: revision and dirty-patch hash, binary and demo SHA-256, toolchain, adapter, window, render target and the viewport the passes were sized by, pools, affinity, env toggles, cache state by kind. An absent fact isnull, never a default;summary.json— spans, counters and audio totals as data;frames.csv— one row per frame: each span's overlap with it, every counter,covered_ns,main_covered_ns,carried_in_ns, flags, thread slots. GPU counters aredelivered_*: a timestamp arrives frames after the work that queued it;load_jobs.csv— one row per load job:discovered → plan_ready → enqueued → started → decode_started → finished → joined.budget_wait_msis a worker held by the image memory threshold,decode_msthe work;prepared_bytesis what the job decoded itself andreused_byteswhat it served out of another job's payload without decoding anything, against what the merge kept and threw away. Onlyprepared_bytesis work.
A diagnostic build with --features bevy-trace writes Bevy's own tracing
spans — the ones inside the render graph, queue_submit among them — as a
Chrome trace. It costs the frame it measures, so it is never a timed run;
scheduling.bevy_tracing in the manifest says whether a run was one.
Render-world handoff spans are receive_render_world, extract_body, and
dispatch_render_world. Receiving can execute worker/main-thread tasks as well
as wait; it is not CPU idle time. Older reports' extract_wait included all
three phases and cannot be compared directly with the receive span.