perf: hoist update() inner functions out of the tick path

updateIfIdle and updateIfWaitingToRoll were declared inside update(),
allocating two closures on every 32ms tick. They are now module-level
functions receiving the state explicitly. Behaviour is unchanged, as
proven by the characterisation suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVTnCfesGNGwPQJwJ9FwoD
This commit is contained in:
Claude
2026-07-04 09:57:06 +00:00
parent b10c38514f
commit 56257adcbd
+28 -26
View File
@@ -637,7 +637,7 @@ export function updateCore(state: RuntimeState): UpdateResult {
// 2. are we waiting to roll? // 2. are we waiting to roll?
if (state.timer.playback === Playback.Roll && state.timer.secondaryTimer !== null) { if (state.timer.playback === Playback.Roll && state.timer.secondaryTimer !== null) {
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, now); const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, now);
return updateIfWaitingToRoll(clockHasCrossedMidnight); return updateIfWaitingToRoll(state, clockHasCrossedMidnight);
} }
// 3. at this point we know that we are playing an event // 3. at this point we know that we are playing an event
@@ -674,36 +674,38 @@ export function updateCore(state: RuntimeState): UpdateResult {
getExpectedTimesCore(state); getExpectedTimesCore(state);
return { hasTimerFinished: finishedNow, hasSecondaryTimerFinished: false }; return { hasTimerFinished: finishedNow, hasSecondaryTimerFinished: false };
}
function updateIfIdle() { // module level so the hot path does not allocate a closure on every tick
// if nothing is running, nothing to do function updateIfIdle(): UpdateResult {
return { hasTimerFinished: false, hasSecondaryTimerFinished: false }; // if nothing is running, nothing to do
return { hasTimerFinished: false, hasSecondaryTimerFinished: false };
}
// module level so the hot path does not allocate a closure on every tick
function updateIfWaitingToRoll(state: RuntimeState, hasCrossedMidnight: boolean): UpdateResult {
// eslint-disable-next-line no-unused-labels -- dev code path
DEV: {
if (state.eventNow === null || state._timer.secondaryTarget === null) {
throw new Error('runtimeState.updateIfWaitingToRoll: invalid state received');
}
} }
function updateIfWaitingToRoll(hasCrossedMidnight: boolean) { // account for offset
// eslint-disable-next-line no-unused-labels -- dev code path const offsetClock = state.clock + state.offset.absolute;
DEV: { state.timer.phase = TimerPhase.Pending;
if (state.eventNow === null || state._timer.secondaryTarget === null) {
throw new Error('runtimeState.updateIfWaitingToRoll: invalid state received');
}
}
// account for offset if (hasCrossedMidnight) {
const offsetClock = state.clock + state.offset.absolute; // if we crossed midnight, we need to update the target
state.timer.phase = TimerPhase.Pending; // this is the same logic from the roll function
state._timer.secondaryTarget = normaliseRollStart(state.eventNow.timeStart, offsetClock) as TimeOfDay;
if (hasCrossedMidnight) {
// if we crossed midnight, we need to update the target
// this is the same logic from the roll function
state._timer.secondaryTarget = normaliseRollStart(state.eventNow.timeStart, offsetClock) as TimeOfDay;
}
state.timer.secondaryTimer = state._timer.secondaryTarget! - offsetClock;
return {
hasTimerFinished: false,
hasSecondaryTimerFinished: state.timer.secondaryTimer <= 0,
};
} }
state.timer.secondaryTimer = state._timer.secondaryTarget! - offsetClock;
return {
hasTimerFinished: false,
hasSecondaryTimerFinished: state.timer.secondaryTimer <= 0,
};
} }
export function roll( export function roll(