# Performance loop (v2.6.0)
You run unattended for hours. Each wake-up does **one item**: measure, fix,
verify, commit, record. The tracker is the memory between wake-ups, because
your context is compacted along the way. Follow `AGENTS.md` in full; this file
only adds to it.
## Where
- Work only in `/home/vasylt/Code/rusting/RustingEngine-performance` on branch
`lane/performance`. Never edit, commit to, merge into or push `main` or any
other lane's folder. The owner merges.
- Tracker: `performanceImprovement.md` in the lane, committed with the work.
## Step 0: setup (only when the lane has no tracker yet)
1. `cd /home/vasylt/Code/rusting/RustingEngine-performance`, check that
`git status` is clean, then `git rebase main`.
2. Copy `/home/vasylt/Code/rusting/RustingEngine/performanceImprovement.md`
into the lane. Append a `## Loop log` section at the end, then commit it
alone: "Add the v2.6.0 performance plan".
## Each wake-up
1. **Orient.** Run `cd` to the lane. If `git status` is not clean, a previous
wake-up was cut off. Read the last `## Loop log` entry and either finish
that item or `git stash push -m "<ID> interrupted"` and log it. Then run
`git rebase main`. On a conflict that is not trivial, run
`git rebase --abort`, log it, and continue on the current base.
2. **Pick.** Take the first open (`- [ ]`) item in the order of "Order and
lanes", then by phase. Skip:
- items in "Owner decisions" (see below),
- items marked `[-]` (blocked) or `[x]` (done),
- items another lane holds (`scripts/lane.sh claims`).
Claim it with `scripts/lane.sh claim "<ID>"`. Before you continue, read
every file the item touches and trace the real flow end to end. Line
numbers in the tracker drift.
3. **Measure before.** Use the command in the item's gate or verify line.
Otherwise use the closest benchmark below. Record the exact command and
the result.
4. **Fix.** Make the smallest change that removes the cost at its root.
- Add no dependency.
- Keep the physics determinism rules. The bit-identical guards in
"Ground rules" must pass unchanged, unless the item says it re-records
them.
- Golden images stay unchanged, unless the item is a declared quality
trade.
- Every change in editor scene edits goes through the snapshot undo.
- Add the smallest test that fails if the gain regresses (a counter, a
call count, an allocation or byte count) when the item has no test yet.
5. **Measure after,** with the same command. Take the median of 3 runs for
times. Counters need one run.
6. **Full check.** Run every command in the AGENTS.md full check, with the
gpu-tests run through `flock /tmp/rusting-gpu.lock`.
7. **Decide.**
- **It passes and the number moved:** tick the item `[x]` in the tracker.
Under the item, add `Done:` with the before and after numbers, the
command, and the test names. Add one line to the `CHANGELOG.md`
`[Unreleased]` `### Changed` section when users can feel the change.
Commit one item per commit, and end the message with
`Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`. Then run
`scripts/lane.sh done "<ID>"`.
- **The check fails and you cannot fix it inside the item, or the gain is
under the noise:** run `git stash push -m "<ID> <reason>"`. Mark the
item `[-]` with one `Blocked:` line naming the reason and the stash.
Commit only the tracker, drop the claim, and move on. Never loop on one
item: after 3 failed attempts, block it.
- **The item is too big for one commit** (most of Phase 3): split it
into `<ID>a`, `<ID>b` and so on under the item. Do the first part now.
8. **Log.** Append one line to `## Loop log`: date and time, ID, result,
commit hash, and the next item.
9. **Continue.** Call ScheduleWakeup with `delaySeconds: 60`, then end the
turn.
## Discovery sweep (every 5 committed items, and when the list runs dry)
You must also find work that is not in the tracker yet.
1. Run the benchmark set below, and compare it with the Baseline and Round 2
tables.
2. Profile the worst case with Nsight Systems on a frame-pointer build:
`RUSTFLAGS="-C force-frame-pointers=yes" cargo build --release --example <name>`,
then `nsys profile --sample=cpu --trace=vulkan,osrt -o <scratch>/<name> <binary> ...`.
Read the top self and inclusive functions with `nsys stats`.
3. Read code that no round has audited yet. `grep` for these patterns:
- `clone()` in per-frame loops
- `Vec::new`, `collect` or `format!` in per-frame or per-tick paths
- `HashMap` with SipHash on hot paths
- linear `find` or `position` scans on large lists
- `world.query` without a cached state
- work done every frame with no change check
4. For each new cost above 2% of a frame or step, or above 1 MB a frame,
append an item with prefix **P** (P1, P2, …) under `## Found by the loop`
in the tracker. Use the same form as the other items: the problem with a
file and line, the evidence number, the fix, and a gate.
5. Commit the tracker alone ("Add loop findings P<n>-P<m>").
## Benchmarks
Write every output to `target/perf-loop/`, never to `benchmarks/`.
`render_bench` compares against an existing JSON file, and writes the file
when it does not exist. Use a new file name per run, or delete the old file.
```sh
flock /tmp/rusting-gpu.lock cargo run --release --example render_bench -- eco target/perf-loop/<ID>-<n>-eco.json
flock /tmp/rusting-gpu.lock cargo run --release --example render_bench -- high target/perf-loop/<ID>-<n>-high.json
flock /tmp/rusting-gpu.lock cargo run --release --example render_bench -- eco target/perf-loop/<ID>-<n>-bears.json --bears 50000 --bear-updates 50
flock /tmp/rusting-gpu.lock env RUSTING_PERF=1 cargo run --release --features window --example physics_bench -- falling 100000
cargo run --release --example physics_suite
```
- The GPU is shared with jobs outside the lock. Before timing, run
`nvidia-smi --query-compute-apps=pid,process_name --format=csv`. Note any
other job next to the numbers. Counters (bytes, draws, dispatches, calls)
are exact, so trust them over times.
- `/tmp` is a 16 GB tmpfs. Keep Nsight files in the session scratchpad, and
delete them after you read them. Keep `target/perf-loop/` under 1 GB.
## Owner decisions: measure, never implement
X1, X5, X17 and any new dependency or build-profile change need the owner's
approval. Run the measurement the tracker names (for X5, the `LD_PRELOAD`
jemalloc and tcmalloc runs), write the result under the item, and leave it
`[ ]`. Treat any other change that needs a format break, a re-recorded hash,
or a visible quality loss the same way, unless the item already approves it.
## Stop
Call ScheduleWakeup with `stop: true` when one of these is true:
- **The list is finished.** Every item outside "Owner decisions" is `[x]`
or `[-]`, and a discovery sweep found nothing new.
- **The build is blocked.** `main` cannot build, or the GPU lock or driver
stays broken for 3 wake-ups in a row.
Before stopping, write a `## Loop summary` to the tracker and commit it. It
lists:
- the acceptance gates, each with the old number, the new number, and pass
or fail,
- the blocked items and why,
- the owner decisions with their measurements.