makepad/skills/makepad-debug-profiler/references/profiling.md
Arena Agent fe21c07d84 skills: add Makepad game skills pack ported from threejs-game-skills
Nine agent skills for building Makepad/Rust games, ported from
majidmanzarpour/threejs-game-skills. Same director-routed workflow and
premium bar; runtime rewritten for this fork's game crates.

- makepad-game-director        entrypoint, routing, continuity, asset probe
- makepad-gameplay-systems     loop, movers vs rigid bodies, input, camera, netplay
- makepad-aaa-graphics-builder lighting, shaders, budget, visual scorecard
- makepad-game-ui-designer     HUD, menus, touch and XR UI
- makepad-debug-profiler       defect bisection and profiling
- makepad-qa-release           verification ladder, evidence, packaging
- makepad-3d-generator         CC0 model search, casts, procedural geometry
- makepad-image-generator      texgen textures, palettes, sky, icons
- makepad-audio-generator      sample bank, mixer, material impacts, 3D audio

Written against the real APIs in libs/game/*, libs/sim and apps/arcade:
the game.* verb table, GameRenderer adaptive quality, the packed 6-float
GameMeshVertex layout, script_mod! splash styling, makepad-test driving,
and the BUDGETS.md numbers. No paid generation API is required - the CC0
library plus seeded makepad-game-gen replaces them.

Includes install.sh (Codex/Claude), validate-skills.sh and a repo-aware
probe_assets.sh; both scripts verified against this checkout.
2026-09-05 21:18:58 +00:00

3.9 KiB
Raw Permalink Blame History

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

renderer.report_frame_ms(ms);   // feed every frame; returns whether quality changed
renderer.frame_p90_ms();        // Option<f32>
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);
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.