Fix ecs_progress blocking when the clock does not advance between frames - #2247
Open
simon112 wants to merge 1 commit into
Open
Fix ecs_progress blocking when the clock does not advance between frames#2247simon112 wants to merge 1 commit into
simon112 wants to merge 1 commit into
Conversation
simon112
force-pushed
the
fix/frame-stalled-clock
branch
2 times, most recently
from
August 9, 2026 15:33
a61eb53 to
466a3de
Compare
Frame time measurement retried in a loop until the clock returned a nonzero delta. On a clock that is too coarse to measure a short frame (a browser with reduced timer precision, a coarse OS tick), every frame that finishes within one clock quantum busy-spins reading the clock until the quantum passes. On a clock that only advances when the host steps it in between frames (emscripten without asyncify, simulated clocks) the loop never exits and ecs_progress blocks forever. A zero measurement now reports a minimal nonzero delta (ECS_FRAME_MIN_DELTA_TIME) rather than measuring again until the clock moves. Because the frame start time is written back value-identical when the clock did not advance, the elapsed time is credited in full to the frame in which the clock next advances, so no time is lost or invented beyond the reported minimum. Reporting a nonzero delta also keeps every existing divisor of delta_time safe. A delta of zero is already observable today (time_scale 0, OnStart systems), so this changes no contract for consumers. The first frame that reports the minimum logs a warning, once per world, so a rate that was computed by dividing by it can be traced to the clock rather than debugged as a spike. The first-frame branch is now selected by a world flag instead of testing the frame start time against zero, which pinned a simulated clock that starts at 0 to the first-frame branch forever. The world summary computed fps as 1.0 / delta_time_raw, which is inf before the first measured frame (not representable in JSON on the REST endpoint) and a rate in the billions on frames where the clock did not advance. It now reports 0 when no measurable frame time exists, and the windowed fps gauge in the stats addon applies the same threshold: a stats window averaging at or below the minimum delta reports 0 instead of the reciprocal of the minimum. Tests cover a stalled clock, a stalled clock that reads 0, the warning firing exactly once across repeated stalled frames, and a coarse clock, including that measured deltas add up to how far the clock actually moved; the stalled clock stats test pins the summary and the gauge together. Note: with a target FPS configured a stalled clock still blocks, in the frame rate limiting loop of flecs_insert_sleep. That is a separate defect with its own fix and tests, submitted as a follow-up PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simon112
force-pushed
the
fix/frame-stalled-clock
branch
from
August 9, 2026 16:16
466a3de to
c7c3d0e
Compare
Contributor
Author
|
[Edit: below being written by AI, as was everything else in this thread except this edit, the WASM application also being vibecoded by me (but AI sessions in different folders treat each other as separate projects), permission being from me.] For the record, a field signature of this wedge from a downstream WASM application (shared with permission), in case anyone else is trying to match a report against it:
The host was Emscripten, where |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
(AI-authored fix; verification described at the bottom.)
Problem
flecs_start_measure_frameretries measurement in a loop until the clock returns a nonzero delta ("Keep trying while delta_time is zero"). Two consequences:get_timeon a virtualized timeline), the loop never exits andecs_progress()blocks forever.The loop dates from f1164f4 (2019). At the time, a zero measurement risked losing the elapsed time, so retrying was the conservative choice. Measurement has since come to preserve the frame start time across a zero reading (
ecs_time_measurewrites back a value-identical timestamp when the clock did not advance), which is what makes it safe to stop retrying.Fix
A zero measurement now reports a minimal nonzero delta (
ECS_FRAME_MIN_DELTA_TIME, 1e-9) instead of measuring again. Since the frame start anchor is preserved, the elapsed time is credited in full to the frame in which the clock next advances: summed deltas still equal actual clock movement (the new coarse-clock test asserts conservation both ways), and the only distortion is the 1e-9 per stalled frame itself. The first frame that reports the minimum logs a warning, once per world: a rate computed by dividing by the minimum is a plausible looking number rather than the inf a zero would produce, so the loudness that a zero delta would have provided moves into the log instead.Why an epsilon and not a fabricated fallback such as 1/60: a fabricated delta advances simulation time that did not pass, which on a fixed-timestep simulation against a stepped clock injects unbounded invented time. The epsilon keeps world time within 1e-9 per frame of what the clock actually reports. Callers that divide by
delta_timestill never see zero, and a tiny delta is not a new hazard class for systems: upstream already has tests assertingdelta_time == 0paths (time_scale0,OnStart).Two adjacent fixes ride along:
EcsWorldFrameStartTimeSet) instead of testing the frame start time against{0,0}, which pinned a simulated clock that legitimately starts at zero to the first-frame branch forever.fpsas1.0 / delta_time_raw, which isinfbefore the first measured frame (not representable in JSON on the REST endpoint) and a rate in the billions on stalled frames. It now reports 0 when no measurable frame time exists, and the windowed fps gauge in the stats addon applies the same threshold, so a stats window averaging at or below the minimum delta also reports 0 rather than the reciprocal of the minimum.Tests
World_progress_w_stalled_clock,World_progress_w_stalled_clock_at_zero,World_progress_w_coarse_clock(conservation with upper and lower bounds, plus clock read-count bounds so a measurement path that spins or gives up fails rather than passes), andStats_world_summary_fps_w_stalled_clock. The stalled-clock tests hang on current master by construction.Note: with a target FPS configured, a stalled clock still blocks in
flecs_insert_sleep. That is a separate defect in the frame rate limiting loop with its own fix and tests, submitted as a follow-up PR (based on this branch); its tests would hang this PR's CI, which is why the fps-variant tests live there.Verified with the full
test/coreandtest/addonssuites on Linux, in debug and sanitize builds. The newWorldtests also pass on Windows/MSVC.distr/regenerated from a clean tree.🤖 Generated with Claude Code