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).
* @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')
throw new TypeError('createAppClock needs a now() function');
// Rate 1: wall + offsetMs, exact. Any other rate: anchorValue plus the wall
// time since anchorSource, scaled.
let offsetMs = 0;
let rateValue = 1;
let anchorValue = 0;
let anchorSource = 0;
let frozen = null;
let latched = null;
// time since anchorSource, scaled. Numbers live in object fields (stored in
// place), not closure variables, so the per-frame latch allocates nothing.
const state = {
offsetMs: 0,
rate: 1,
anchorValue: 0,
anchorSource: 0,
frozen: false,
frozenMs: 0,
latched: false,
latchedMs: 0,
};
const running = () =>
rateValue === 1
? source() + offsetMs
: anchorValue + (source() - anchorSource) * rateValue;
const now = () => (frozen === null ? running() : frozen);
state.rate === 1
? source() + state.offsetMs
: state.anchorValue + (source() - state.anchorSource) * state.rate;
const now = () => (state.frozen ? state.frozenMs : running());
// Make the running clock read `value` from this moment on.
const runFrom = (value) => {
if (rateValue === 1) offsetMs = value - source();
if (state.rate === 1) state.offsetMs = value - source();
else {
anchorValue = value;
anchorSource = source();
state.anchorValue = value;
state.anchorSource = source();
}
};
return {
now,
frameTime: () => (latched === null ? now() : latched),
frameTime: () => (state.latched ? state.latchedMs : now()),
beginFrame() {
latched = now();
return latched;
state.latchedMs = now();
state.latched = true;
return state.latchedMs;
},
freeze(ms) {
frozen = ms === undefined ? now() : finite(ms, 'Frozen time');
state.frozenMs = ms === undefined ? now() : finite(ms, 'Frozen time');
state.frozen = true;
},
resume() {
if (frozen === null) return;
const value = frozen;
frozen = null;
runFrom(value);
if (!state.frozen) return;
state.frozen = false;
runFrom(state.frozenMs);
},
offset(ms) {
const value = source() + finite(ms, 'Clock offset');
frozen = null;
state.frozen = false;
runFrom(value);
},
rate(rate) {
if (!(isFiniteNumber(rate) && rate >= 0))
throw new TypeError(`Clock rate must be a finite number >= 0: ${rate}`);
if (frozen !== null) {
rateValue = rate;
if (state.frozen) {
state.rate = rate;
return;
}
const value = running();
rateValue = rate;
state.rate = rate;
runFrom(value);
},
reset() {
offsetMs = 0;
rateValue = 1;
frozen = null;
state.offsetMs = 0;
state.rate = 1;
state.frozen = false;
},
state: () => ({
frozen: frozen !== null,
frozen: state.frozen,
offsetMs: now() - source(),
rate: rateValue,
rate: state.rate,
}),
};
}