perf(time): the app clock's frame latch without closure boxing

The clock keeps its numbers in object fields (stored in place) instead of
closure variables, and its default wall clock is one module-level
function. Micro-measure (Node, --expose-gc, 200k calls): the flights
render-time arithmetic allocates about 25-35 B per call with the app
clock against 66-102 B with JulianDate.now/addSeconds; an engine tick
(beginFrame + ticked) about 20-30 B.
This commit is contained in:
German Khokhlov committed 2026-10-16 15:03:23 +04:00
1 parent c33ee31efc
commit bb4e6bafa4
1 file changed
+42 -32
+42 -32
View File
@@ -38,73 +38,83 @@ function finite(value, label) {
* @param {{ now?: () => number }} [options] `now`: the wall clock (Date.now). * @param {{ now?: () => number }} [options] `now`: the wall clock (Date.now).
* @returns {AppClock} * @returns {AppClock}
*/ */
export function createAppClock({ now: source = () => Date.now() } = {}) { // The wall clock, read through Date.now at each call (so a mocked Date.now
// is the app clock's too).
const wallClock = () => Date.now();
export function createAppClock({ now: source = wallClock } = {}) {
if (typeof source !== 'function') if (typeof source !== 'function')
throw new TypeError('createAppClock needs a now() function'); throw new TypeError('createAppClock needs a now() function');
// Rate 1: wall + offsetMs, exact. Any other rate: anchorValue plus the wall // Rate 1: wall + offsetMs, exact. Any other rate: anchorValue plus the wall
// time since anchorSource, scaled. // time since anchorSource, scaled. Numbers live in object fields (stored in
let offsetMs = 0; // place), not closure variables, so the per-frame latch allocates nothing.
let rateValue = 1; const state = {
let anchorValue = 0; offsetMs: 0,
let anchorSource = 0; rate: 1,
let frozen = null; anchorValue: 0,
let latched = null; anchorSource: 0,
frozen: false,
frozenMs: 0,
latched: false,
latchedMs: 0,
};
const running = () => const running = () =>
rateValue === 1 state.rate === 1
? source() + offsetMs ? source() + state.offsetMs
: anchorValue + (source() - anchorSource) * rateValue; : state.anchorValue + (source() - state.anchorSource) * state.rate;
const now = () => (frozen === null ? running() : frozen); const now = () => (state.frozen ? state.frozenMs : running());
// Make the running clock read `value` from this moment on. // Make the running clock read `value` from this moment on.
const runFrom = (value) => { const runFrom = (value) => {
if (rateValue === 1) offsetMs = value - source(); if (state.rate === 1) state.offsetMs = value - source();
else { else {
anchorValue = value; state.anchorValue = value;
anchorSource = source(); state.anchorSource = source();
} }
}; };
return { return {
now, now,
frameTime: () => (latched === null ? now() : latched), frameTime: () => (state.latched ? state.latchedMs : now()),
beginFrame() { beginFrame() {
latched = now(); state.latchedMs = now();
return latched; state.latched = true;
return state.latchedMs;
}, },
freeze(ms) { freeze(ms) {
frozen = ms === undefined ? now() : finite(ms, 'Frozen time'); state.frozenMs = ms === undefined ? now() : finite(ms, 'Frozen time');
state.frozen = true;
}, },
resume() { resume() {
if (frozen === null) return; if (!state.frozen) return;
const value = frozen; state.frozen = false;
frozen = null; runFrom(state.frozenMs);
runFrom(value);
}, },
offset(ms) { offset(ms) {
const value = source() + finite(ms, 'Clock offset'); const value = source() + finite(ms, 'Clock offset');
frozen = null; state.frozen = false;
runFrom(value); runFrom(value);
}, },
rate(rate) { rate(rate) {
if (!(isFiniteNumber(rate) && rate >= 0)) if (!(isFiniteNumber(rate) && rate >= 0))
throw new TypeError(`Clock rate must be a finite number >= 0: ${rate}`); throw new TypeError(`Clock rate must be a finite number >= 0: ${rate}`);
if (frozen !== null) { if (state.frozen) {
rateValue = rate; state.rate = rate;
return; return;
} }
const value = running(); const value = running();
rateValue = rate; state.rate = rate;
runFrom(value); runFrom(value);
}, },
reset() { reset() {
offsetMs = 0; state.offsetMs = 0;
rateValue = 1; state.rate = 1;
frozen = null; state.frozen = false;
}, },
state: () => ({ state: () => ({
frozen: frozen !== null, frozen: state.frozen,
offsetMs: now() - source(), offsetMs: now() - source(),
rate: rateValue, rate: state.rate,
}), }),
}; };
} }