Skip to content

refactor(loop): extract per-frame work into tick() so try/catch stays in a thin shell#116

Open
chiefcll wants to merge 1 commit into
mainfrom
loop/extract-tick-from-runloop
Open

refactor(loop): extract per-frame work into tick() so try/catch stays in a thin shell#116
chiefcll wants to merge 1 commit into
mainfrom
loop/extract-tick-from-runloop

Conversation

@chiefcll

Copy link
Copy Markdown
Contributor

What

Splits WebPlatform.startLoop into two closures so the try/catch keep-alive guard no longer wraps the hot per-frame body directly:

  • tick(currentTime) — all per-frame work: context-lost check, frame limiting, updateFrameTime/updateAnimations, the idle path, and the active drawFrame/flushFrameEvents path. This is where every client callback (the only code that can throw) and every hot stage.* call runs. No try/catch → fully optimizable.
  • runLoop(currentTime) — a near-empty shell that is essentially just try { tick(t) } catch (e) { … }. It carries the only try/catch, so it's the only function de-optimized on older engines — and it does almost nothing.

Why

The render-loop keep-alive guard (#115) wraps the whole frame body in a try/catch inside the rAF callback. Some optimizing compilers — notably Chrome 38's Crankshaft, our language floor — refuse to optimize any function containing a try/catch, so the entire hot per-frame function was being de-optimized on exactly the embedded targets we care about.

Extracting the body confines the de-opt to the thin shell. The genuinely hot code (drawFrame, updateAnimations, idle bookkeeping) lives in tick and the stage.* methods, all still optimizable. This is the standard "don't put try/catch in hot functions; extract" guidance applied literally.

Context: this is the local twin of the upstream discussion on lightning-js/renderer#819, where the try/catch-on-the-hot-path cost was raised in review.

Reviewer notes — behavior is unchanged

  • The scheduled double-schedule guard is preserved: promoted to a closure variable, reset at the top of runLoop before the try, and set by tick at each point it queues the next frame. The catch still reschedules only when tick had not already.
  • Context-lost early-return, frame-limit skip, idle vs. active paths, setTimeout/rAF scheduling, and the no-op-default handleLoopError semantics are all identical.
  • Context-lost and frame-limit paths now run inside tick (nominally under the try), but they execute no client code and cannot throw, so the catch never fires for them — and that per-frame work now runs in the optimizable function, which is where you want it.

Verification

  • tsc --noEmit — clean
  • eslint — clean
  • No other Platform implements startLoop, so nothing else to mirror.

🤖 Generated with Claude Code

… in a thin shell

The render-loop keep-alive guard added in #115 wraps the whole frame body in a
try/catch inside the rAF callback. Some optimizing compilers — notably Chrome
38's Crankshaft, our language floor — refuse to optimize any function that
contains a try/catch, so the entire hot per-frame function was being
de-optimized on the embedded targets we care about.

Split startLoop into two closures:

- tick(currentTime): all per-frame work — context-lost check, frame limiting,
  updateAnimations, the idle path, and the active drawFrame/flushFrameEvents
  path. This is where every client callback (the only code that can actually
  throw) and every hot stage.* call runs. No try/catch, so it stays fully
  optimizable.
- runLoop(currentTime): a near-empty shell that is just `try { tick(t) } catch`.
  It carries the only try/catch, so it's the only function de-optimized on
  Crankshaft — and it does almost nothing.

Behavior is unchanged. The `scheduled` double-schedule guard is promoted to a
closure variable, reset at the top of runLoop before the try and set by tick at
each scheduling point, so the catch still reschedules only when tick had not
already queued the next frame. Context-lost and frame-limit paths now run inside
tick but execute no client code and cannot throw, so the catch never fires for
them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant