# Profiling ## Rules 1. **Measure first.** Optimising an unmeasured guess usually makes something else slower. 2. **`--release` only.** Debug frame times are meaningless for shipping decisions. 3. **Report p90, not mean.** A 16 ms mean with a 40 ms p90 stutters, and the mean hides it. 4. **Measure on the target device class.** The BUDGETS Quest column is an estimate until it runs on device. ## Instruments ```rust renderer.report_frame_ms(ms); // feed every frame; returns whether quality changed renderer.frame_p90_ms(); // Option renderer.quality_level(); renderer.quality_reason(); // why quality dropped - read this before debugging "missing" effects renderer.shadow_budget(); stats.instance_floats; // RenderStats, from the compiled shader renderer.model_triangles(id); ``` ```bash cargo run -p makepad-arcade --example bigworld_probe --release ``` ## Where time goes From `apps/arcade/BUDGETS.md`: - Sim: 100 movers + 50 rigid bodies + 65×65 terrain = **0.038 ms/tick** against 16.6 ms. **Gameplay code is essentially never the problem.** - Script: ≤ 2 ms, one cumulative 500k-instruction pool per tick. - The real costs are **draw calls** and **CPU skinning**. Skinning is CPU-side, so a skinned character's entire vertex buffer re-uploads every frame. The Knight: 3716 verts × 64 B = 238 KB/frame unpacked, 89 KB packed. Skinned character count is the highest-leverage number in most scenes. ## Bandwidth first Quest is bandwidth-bound before ALU-bound. | stream | packed | | --- | --- | | cube instance | 32 floats / 128 B (was 44 / 176 B) | | skinned vertex | 6 floats / 24 B (was 16 / 64 B) | | shadow mesh vertex | 6 floats / 24 B | `geom.GameMeshVertex`: position 3 exact f32, octahedral normal 1 lane, uv 2×f16 1 lane, colour 4×unorm8 1 lane. Attributes are f32-only in this engine; compression is bit-packing plus `unpack2f16` / `unpack4u8` in-shader. Anything constant across a batch belongs in a uniform. Moving `sun_color`, `sun_sky`, `sun_ground`, `fog_color` off the instance stream saved 27% per cube. ## Cut order 1. Instance/vertex bytes (packed layout, batch constants → uniforms) 2. Draw calls (batch by material and mesh) 3. Skinned character count (cap; degrade distant ones) 4. Shadow casters (`set_shadow_budget`; bake AO for the rest) 5. Particle pool cap 6. Terrain resolution (uploaded per revision, not per frame) 7. Fragment work — last, and only with evidence you are ALU-bound ## Stutter Different problem from low framerate. Causes, in order of likelihood: allocation inside the frame loop, model loading during play, terrain regeneration on a gameplay event, container rehash in `step`, unbounded `Vec` growth (particles, `BulletDecal`, logs, the gen `cache`), and GC-like bulk frees. Fix by preloading and pooling. Anything created per frame should be reused per frame. ## Memory Watch: model cache (`makepad-game-gen::cache`), decal buffers (cap and recycle — `DecalBuffer` has `clear`, `push`, `len`), particle pools, audio voices (`Mixer` has `Priority` for a reason — cap voices and let priority evict), and log buffers. The asset index itself is cheap: ~2.1 MB heap and ~120 ms build for 4,999 entries in release. It is not your leak. ## Two-controller oscillation `GameRenderer` already implements adaptive quality. A second frame pacer or a settings slider that overrides it will oscillate and produce worse frame times than either alone. Surface `quality_level` / `set_shadow_budget` / `set_refresh_hz` as a *ceiling* and let the adaptive controller work beneath it. ## Reporting a performance result Always give before and after, in release, on a named device, with p90: ``` device M1 Air, release before p90 24.3 ms, q1 (reason: frame_time), 41 skinned, 96 casters after p90 11.8 ms, q3, 12 skinned (distant LOD'd), 24 casters change packed skinned vertex 64B->24B; shadow budget 96->24; batched crates budget 16.6 ms @ 60 Hz - 29% headroom ``` A performance claim without a before number is not a claim.