UNPKG

tempus

Version:
368 lines (365 loc) 10.6 kB
// packages/core/src/clock.ts var Clock = class { _elapsed = 0; _currentTime = 0; _startTime = void 0; _lastTime = void 0; _isPlaying = false; _deltaTime = 0; play() { if (this._isPlaying) return; this._currentTime = 0; this._startTime = void 0; this._isPlaying = true; } pause() { if (!this._isPlaying) return; this._deltaTime = 0; this._isPlaying = false; } reset() { this._elapsed = 0; this._deltaTime = 0; this._currentTime = 0; this._lastTime = void 0; this._isPlaying = false; } update(browserTime) { if (!this._isPlaying) return; if (!this._startTime) { this._startTime = browserTime; } if (this._lastTime === void 0) { this._lastTime = this._startTime; this._currentTime = 0; this._deltaTime = 0; } else { this._lastTime = this._currentTime; this._currentTime = browserTime - this._startTime; this._deltaTime = this._currentTime - this._lastTime; this._elapsed += this._deltaTime; } } get time() { return this._elapsed; } get isPlaying() { return this._isPlaying; } get deltaTime() { return this._deltaTime; } }; // packages/core/src/uid.ts var index = 0; function getUID() { return index++; } // package.json var version = "1.0.0-dev.18"; // packages/core/src/tempus.ts var isClient = typeof window !== "undefined"; var originalRAF = isClient && window.requestAnimationFrame.bind( window ); var originalCancelRAF = isClient && window.cancelAnimationFrame.bind( window ); if (isClient) { ; window.tempusVersion = version; } function stopwatch(callback) { const now = performance.now(); callback(); return performance.now() - now; } var SAMPLE_WINDOW = 60; function pushSample(samples, duration) { samples.push(duration); if (samples.length > SAMPLE_WINDOW) samples.shift(); } var PATCH_STALE_FRAMES = 120; var Framerate = class { callbacks = []; fps; time = 0; lastTickDate = 0; framesCount = 0; constructor(fps = Number.POSITIVE_INFINITY) { this.fps = fps; } get isRelativeFps() { return typeof this.fps === "string" && this.fps.endsWith("%"); } get maxFramesCount() { if (!this.isRelativeFps) return 1; return Math.max(1, Math.round(100 / Number(this.fps.replace("%", "")))); } get executionTime() { if (this.isRelativeFps) return 0; return 1e3 / this.fps; } dispatch(state) { for (let i = 0; i < this.callbacks.length; i++) { const duration = stopwatch(() => { this.callbacks[i]?.callback(state); }); pushSample(this.callbacks[i].samples, duration); } } raf(state) { this.time += state.deltaTime; if (this.isRelativeFps) { if (this.framesCount === 0) { const frameDelta = state.deltaTime; state.deltaTime = state.time - this.lastTickDate; this.lastTickDate = state.time; this.dispatch(state); state.deltaTime = frameDelta; } this.framesCount++; this.framesCount %= this.maxFramesCount; } else { if (this.fps === Number.POSITIVE_INFINITY) { this.dispatch(state); } else if (this.time >= this.executionTime) { this.time = this.time % this.executionTime; const frameDelta = state.deltaTime; state.deltaTime = state.time - this.lastTickDate; this.lastTickDate = state.time; this.dispatch(state); state.deltaTime = frameDelta; } } } add({ callback, order, label }) { if (typeof callback !== "function") { console.warn("Tempus.add: callback is not a function"); return; } const uid = getUID(); this.callbacks.push({ callback, order, uid, label, samples: [] }); this.callbacks.sort((a, b) => a.order - b.order); return () => this.remove(uid); } remove(uid) { this.callbacks = this.callbacks.filter(({ uid: u }) => uid !== u); } }; var TempusImpl = class { framerates = {}; clock = new Clock(); fps; usage = 0; rafId; frameCount = 0; // Frame rate the per-callback `budget` is measured against. At 60fps the // budget is ~16.67ms; callbacks call `state.budget()` to see how much of it // is left and decide whether to do expensive work this frame. targetFps = 60; frameStartTime = performance.now(); get frameBudget() { return 1e3 / this.targetFps; } // The single object handed to every callback each tick. Reused (not // reallocated) to avoid per-frame GC; `budget()` is a live method (built in // the constructor) so it always reflects the time left at the moment called. state; // patch() state patched = false; rafQueue = /* @__PURE__ */ new Map(); rafHandleId = 0; patchedRAF; patchedCancelRAF; flushUnsubscribe; // Per-absorbed-callback timing, keyed by the callback identity so a // self-rescheduling loop keeps one stable row across frames. The map and // array hold the SAME entry objects — the array stays enumerable for the // profiler view, the map gives O(1) identity lookup. patchMeta = /* @__PURE__ */ new WeakMap(); patchEntries = []; patchAnonCount = 0; constructor() { const impl = this; this.state = { time: 0, deltaTime: 0, frame: 0, budget() { return impl.frameBudget - (performance.now() - impl.frameStartTime); } }; if (!isClient) return; this.play(); } restart() { if (this.rafId) { originalCancelRAF(this.rafId); } this.frameCount = 0; for (const framerate of Object.values(this.framerates)) { framerate.framesCount = 0; framerate.time = 0; framerate.lastTickDate = performance.now(); } this.clock.reset(); this.play(); } play() { if (!isClient || this.clock.isPlaying) return; this.clock.play(); this.rafId = originalRAF(this.raf); } pause() { if (!isClient || !this.rafId || !this.clock.isPlaying) return; originalCancelRAF(this.rafId); this.rafId = void 0; this.clock.pause(); } get isPlaying() { return this.clock.isPlaying; } add(callback, { order, priority, fps = Number.POSITIVE_INFINITY, label = "" } = {}) { if (!isClient) return; const resolvedOrder = order ?? priority ?? 0; if (typeof fps === "number" || typeof fps === "string" && fps.endsWith("%")) { if (!this.framerates[fps]) this.framerates[fps] = new Framerate(fps); return this.framerates[fps].add({ callback, order: resolvedOrder, label }); } console.warn('Tempus.add: fps is not a number or a string ending with "%"'); } raf = (browserElapsed) => { if (!isClient) return; this.clock.update(browserElapsed); const elapsed = this.clock.time; const deltaTime = this.clock.deltaTime; this.fps = 1e3 / deltaTime; this.frameStartTime = performance.now(); this.state.time = elapsed; this.state.deltaTime = deltaTime; this.state.frame = this.frameCount; const duration = stopwatch(() => { for (const framerate of Object.values(this.framerates)) { framerate.raf(this.state); } }); if (deltaTime) { this.usage = duration / deltaTime; } this.frameCount++; this.rafId = originalRAF(this.raf); }; patch() { if (!isClient || this.patched) return; this.patched = true; this.flushUnsubscribe = this.add( () => { if (this.rafQueue.size === 0) return; const batch = Array.from(this.rafQueue.values()); this.rafQueue.clear(); const now = performance.now(); const frame = this.frameCount; for (const callback of batch) { let meta = this.patchMeta.get(callback); if (!meta) { meta = { callback, label: callback.name || `anonymous#${++this.patchAnonCount}`, samples: [], lastFrame: frame }; this.patchMeta.set(callback, meta); this.patchEntries.push(meta); } const duration = stopwatch(() => { try { callback(now); } catch (error) { console.error("Tempus.patch: rAF callback threw", error); } }); pushSample(meta.samples, duration); meta.lastFrame = frame; } this.prunePatchEntries(frame); }, { label: "tempus" } ); this.patchedRAF = (callback) => { const id = ++this.rafHandleId; this.rafQueue.set(id, callback); return id; }; window.requestAnimationFrame = this.patchedRAF; this.patchedCancelRAF = (handle) => { this.rafQueue.delete(handle); }; window.cancelAnimationFrame = this.patchedCancelRAF; } unpatch() { if (!isClient || !this.patched) return; if (window.requestAnimationFrame === this.patchedRAF) { window.requestAnimationFrame = originalRAF; } if (window.cancelAnimationFrame === this.patchedCancelRAF) { window.cancelAnimationFrame = originalCancelRAF; } this.flushUnsubscribe?.(); this.flushUnsubscribe = void 0; this.rafQueue.clear(); this.patchedRAF = void 0; this.patchedCancelRAF = void 0; this.patchMeta = /* @__PURE__ */ new WeakMap(); this.patchEntries = []; this.patchAnonCount = 0; this.patched = false; } // Drop rows for loops that stopped rescheduling, so they fall off the // profiler view instead of lingering as stale 0ms entries. prunePatchEntries(frame) { for (let i = this.patchEntries.length - 1; i >= 0; i--) { const entry = this.patchEntries[i]; if (frame - entry.lastFrame > PATCH_STALE_FRAMES) { this.patchMeta.delete(entry.callback); this.patchEntries.splice(i, 1); } } } // Unified timing snapshot for tempus/profiler: every Tempus.add() callback and // every loop absorbed by Tempus.patch(), normalized to one shape. Samples // are copied so consumers can't mutate live state. inspect() { const added = Object.values(this.framerates).flatMap( (framerate) => framerate.callbacks.map((callback) => ({ label: callback.label, samples: callback.samples.slice(), order: callback.order, fps: framerate.fps, source: "add" })) ); const patched = this.patchEntries.map((entry) => ({ label: entry.label, samples: entry.samples.slice(), order: 0, fps: Number.POSITIVE_INFINITY, source: "patch" })); return [...added, ...patched]; } }; var Tempus = new TempusImpl(); export { Tempus as default }; //# sourceMappingURL=tempus.mjs.map