docs(profiling): document screenshot/PNG trap; add full-loop workload

- docs/profiling.md: new "Profiling the render path" section — headless
  step() doesn't render; automation.screenshot() forces a render but PNG
  encoding then dominates (~96% of instructions in libsfml stbi_zlib_compress),
  burying engine work. Guidance: profile update/animation with step()-only,
  subtract stbi_* for the render path. Alludes to the Hybrid Scene
  Serialization wiki proposal (QOI + UI-tree serialization) as a faster
  capture path that would also de-noise screenshot-driven profiling.
- tests/benchmarks/profile_workload.py: reusable full-loop driver (textured
  grid + moving entities + animated nested UI, forced renders). This is the
  workload that surfaced #348.

Refs #345, #348.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-07-10 21:54:39 -04:00
commit ef0408e06e
2 changed files with 119 additions and 0 deletions

View file

@ -52,6 +52,31 @@ counts). Because it's deterministic, any delta is the change, not noise. `kcache
Caveat: Callgrind models an idealized cache and counts instructions, not wall-clock —
great for "did this do less work," not for real-time behavior.
## Profiling the render path — and the screenshot/PNG trap
In headless mode `mcrfpy.step()` does **not** render (the render path is stubbed); the
only way to force a real render is `automation.screenshot()`, which flushes the
off-screen target to a PNG. That has a sharp consequence for profiling: **PNG encoding
dominates the profile**. On a representative full-loop workload, ~96% of instructions
landed in libsfml's `stbi_zlib_compress` / `stbiw__encode_png_line` — the actual engine
render/update/animation work was buried under 3%.
Implications:
- To profile **update / animation** logic, use a `step()`-only workload with **no**
screenshots — the render cost (and the PNG artifact) is absent, so engine work is the
whole profile.
- To profile the **render path** itself, expect to subtract the PNG cost (filter out
`stbi_*` / libsfml-graphics symbols) or drive rendering by another means.
- The per-frame PNG cost is an artifact of the screenshot *serialization format*, not of
rendering. A faster capture path (e.g. QOI encoding, plus a McRogueFace-specific UI-tree
serialization) is explored in the wiki proposal
[Hybrid Scene Serialization](https://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Proposal%3A-Hybrid-Scene-Serialization);
if adopted it would also make screenshot-driven profiling far less noisy.
A ready full-loop workload (textured grid + moving entities + animated nested UI, with
forced renders) lives at `tests/benchmarks/profile_workload.py`.
## perf — real wall-clock sampling
For the interactive game where you need actual time spent (draw calls, SFML/Python cost