CNA-BUG-208: On Emscripten a diagnostics frame excludes the requestAnimationFrame wait, so FrameSample.framesPerSecond is a work rate there
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
The web loop opens the frame scope inside EmscriptenMainLoopCallback and waits for the next animation frame after it returns, so web frame durations and FPS exclude pacing, unlike the native Tick the documentation describes.
- Identifier
CNA-BUG-208- Category
- Bug
- Subsystem
- Diagnostics & Inspector
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Low (a triage suggestion, not a project priority)
- Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- CNA::Diagnostics::FrameSample (durationNs, framesPerSecond) as published by Game's frame scope
Expected behaviour
docs/diagnostics.md: “Native Game::Tick and the Emscripten main-loop callback publish one frame ... The top-level tick measurement includes fixed-step pacing waits”. A frame should mean the same on both loops, or the difference should be documented.
Actual behaviour at TARGET
Native: CNA_DIAGNOSTICS_FRAME_SCOPE() opens at the top of Game::Tick, which contains the fixed-step Delay/yield loop, so a frame spans the pacing wait and framesPerSecond tracks the target rate. Emscripten: Game::RunLoop calls EmscriptenMainLoopCallback(), whose body holds the frame scope and the Game/Tick zone and paces with an accumulator that never waits, and then calls CNA_WaitForAnimationFrame() (an EM_ASYNC_JS await on requestAnimationFrame) outside it. A web frame's durationNs therefore covers event polling, updates and drawing only, and framesPerSecond (1e9 / durationNs) reports how fast that work ran, for example 500 for 2 ms of work at a 60 Hz display.
Source locations
modules/runtime/src/Game.cpp— Game::Tick; Game::EmscriptenMainLoopCallback; Game::RunLoop and CNA_WaitForAnimationFramemodules/diagnostics/src/Diagnostics.cpp— ProcessProvider::CaptureSnapshot computes framesPerSecond from durationNsdocs/diagnostics.md— Frames and built-in engine metrics
Evidence
Checked by reading Game.cpp and Diagnostics.cpp at 009d40f5. Not built for Emscripten and not observed in a browser.
Focused reproduction
No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.
Current tests
FrameHistoryIsBoundedOrderedAndReportsExactFps checks FPS against synthetic frame durations; no test covers the Emscripten loop's frame boundaries.
Regression test
An Emscripten run (or a fake loop test) asserting that a published frame spans the wait between callbacks, or a documentation note that web FPS measures work time.
Blast radius
Emscripten builds at STATS or FULL that read FrameSample in process (the Inspector is not available on the web). Native loops are unaffected.
Workaround
Derive the display rate from consecutive startTimestampNs values instead of framesPerSecond.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Diagnostics: built-in metrics
- Known issues
- Bug index