From e933af7d5badff7b3064ad2df258acba32a9a5b0 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Tue, 19 May 2026 14:03:50 +0200 Subject: [PATCH 1/4] added framecounter --- src/common/CommonTypes.ts | 2 + src/core/Stage.ts | 57 +++++++++++++++++++++------- src/core/lib/fps.ts | 80 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 src/core/lib/fps.ts diff --git a/src/common/CommonTypes.ts b/src/common/CommonTypes.ts index 812b05fe..1015f476 100644 --- a/src/common/CommonTypes.ts +++ b/src/common/CommonTypes.ts @@ -18,6 +18,7 @@ */ import type { CoreNodeRenderState } from '../core/CoreNode.js'; +import type { FrameCounter } from '../core/lib/fps.js'; import type { TextureError } from '../core/TextureError.js'; /** @@ -138,6 +139,7 @@ export type NodeRenderStateEventHandler = ( export interface FpsUpdatePayload { fps: number; contextSpyData: Record | null; + frameCounter: FrameCounter; } /** diff --git a/src/core/Stage.ts b/src/core/Stage.ts index 958d8f37..8cc5fa79 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -57,6 +57,12 @@ import { ColorTexture } from './textures/ColorTexture.js'; import type { Platform } from './platforms/Platform.js'; import type { WebPlatform } from './platforms/web/WebPlatform.js'; import type { RendererMainSettings } from '../main-api/Renderer.js'; +import { + createFrameCounter, + setFpsInterval, + setFrameBuckets, + type FrameCounter, +} from './lib/fps.js'; export type StageOptions = Omit< RendererMainSettings, @@ -65,6 +71,7 @@ export type StageOptions = Omit< textureMemory: TextureMemoryManagerSettings; canvas: HTMLCanvasElement | OffscreenCanvas; fpsUpdateInterval: number; + fpsBuckets?: number[]; eventBus: EventEmitter; platform: Platform | WebPlatform; inspector: boolean; @@ -136,6 +143,8 @@ export class Stage { lastFrameTime = 0; currentFrameTime = 0; elapsedTime = 0; + currentFrameCounter: FrameCounter | null = null; + private timedNodes: CoreNode[] = []; private clrColor = 0x00000000; private fpsNumFrames = 0; @@ -198,6 +207,12 @@ export class Stage { this.animationManager = new AnimationManager(); this.contextSpy = enableContextSpy ? new ContextSpy() : null; + // Set initial frame buckets and FPS update interval for FPS tracking + if (options.fpsBuckets) { + setFrameBuckets(options.fpsBuckets); + } + setFpsInterval(options.fpsUpdateInterval); + let bm = [0, 0, 0, 0] as [number, number, number, number]; if (boundsMargin) { bm = Array.isArray(boundsMargin) @@ -382,9 +397,8 @@ export class Stage { this.lastFrameTime = this.currentFrameTime; this.currentFrameTime = newFrameTime; this.elapsedTime = newFrameTime - this.startTime; - this.deltaTime = !this.lastFrameTime - ? 100 / 6 - : newFrameTime - this.lastFrameTime; + this.deltaTime = + this.lastFrameTime === 0 ? 100 / 6 : newFrameTime - this.lastFrameTime; this.txManager.frameTime = newFrameTime; this.txMemManager.frameTime = newFrameTime; @@ -542,21 +556,38 @@ export class Stage { // If there's an FPS update interval, emit the FPS update event // when the specified interval has elapsed. const { fpsUpdateInterval } = this.options; - if (fpsUpdateInterval) { - this.fpsNumFrames++; - this.fpsElapsedTime += this.deltaTime; - if (this.fpsElapsedTime >= fpsUpdateInterval) { - const fps = Math.round( - (this.fpsNumFrames * 1000) / this.fpsElapsedTime, - ); - this.fpsNumFrames = 0; - this.fpsElapsedTime = 0; + if (fpsUpdateInterval > 0) { + let frameCounter = this.currentFrameCounter; + const eleapsed = this.elapsedTime; + if (frameCounter !== null && frameCounter.end < eleapsed) { this.queueFrameEvent('fpsUpdate', { - fps, + fps: frameCounter.averageFps, contextSpyData: this.contextSpy?.getData() ?? null, + frameCounter: frameCounter, } satisfies FpsUpdatePayload); this.contextSpy?.reset(); + frameCounter = this.currentFrameCounter = createFrameCounter(eleapsed); + } + + if (frameCounter === null) { + frameCounter = this.currentFrameCounter = createFrameCounter(eleapsed); } + + frameCounter.increment(this.deltaTime); + // this.fpsNumFrames++; + // this.fpsElapsedTime += this.deltaTime; + // if (this.fpsElapsedTime >= fpsUpdateInterval) { + // const fps = Math.round( + // (this.fpsNumFrames * 1000) / this.fpsElapsedTime, + // ); + // this.fpsNumFrames = 0; + // this.fpsElapsedTime = 0; + // this.queueFrameEvent('fpsUpdate', { + // fps, + // contextSpyData: this.contextSpy?.getData() ?? null, + // } satisfies FpsUpdatePayload); + // this.contextSpy?.reset(); + // } } } diff --git a/src/core/lib/fps.ts b/src/core/lib/fps.ts new file mode 100644 index 00000000..0509fe08 --- /dev/null +++ b/src/core/lib/fps.ts @@ -0,0 +1,80 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Comcast Cable Communications Management, LLC. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Class that keeps track of the invocations of Context methods when + * the `enableContextSpy` renderer option is enabled. + */ + +export interface FrameCounter { + start: number; + end: number; + frameCount: number; + frames: Record; + increment: (frameDelta: number) => void; + get averageFps(): number; +} + +let buckets = [20, 40, 60, 80, 100]; +let overflowBucketLabel = '>' + buckets[buckets.length - 1]; +let fpsInterval = 1000; // 1 second + +const frameCounter: FrameCounter = { + start: 0, + end: 0, + frameCount: 0, + frames: {}, + increment(frameDelta: number) { + this.frameCount++; + for (let i = 0; i < buckets.length; i++) { + const bucket = buckets[i] as number; + if (frameDelta <= bucket) { + this.frames[bucket]!++; + return; + } + } + this.frames[overflowBucketLabel]!++; + }, + get averageFps() { + //calculate fps based on frame count and elapsed time + return (this.frameCount / (this.end - this.start)) * 1000; + }, +}; + +export function setFrameBuckets(newBuckets: number[]) { + buckets = newBuckets; + overflowBucketLabel = '>' + buckets[buckets.length - 1]; +} + +export function setFpsInterval(newInterval: number) { + fpsInterval = newInterval; +} + +export function createFrameCounter(frameTime: number): FrameCounter { + const counter = Object.create(frameCounter) as FrameCounter; + counter.start = frameTime; + counter.end = frameTime + fpsInterval; + //fill frames with 0 for each bucket + for (let i = 0; i < buckets.length; i++) { + const bucket = buckets[i] as number; + counter.frames[bucket] = 0; + } + counter.frames[overflowBucketLabel] = 0; + return counter; +} From 1e2779b231024b72378d8d9e465c01b8cc8a7598 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Thu, 4 Jun 2026 11:24:00 +0200 Subject: [PATCH 2/4] tweaking property names, change framecount payload --- src/common/CommonTypes.ts | 4 +-- src/core/Stage.ts | 30 ++++++----------- src/core/lib/fps.ts | 70 +++++++++++++++++++++++++-------------- 3 files changed, 58 insertions(+), 46 deletions(-) diff --git a/src/common/CommonTypes.ts b/src/common/CommonTypes.ts index 1015f476..422767e7 100644 --- a/src/common/CommonTypes.ts +++ b/src/common/CommonTypes.ts @@ -18,7 +18,7 @@ */ import type { CoreNodeRenderState } from '../core/CoreNode.js'; -import type { FrameCounter } from '../core/lib/fps.js'; +import type { FrameCount } from '../core/lib/fps.js'; import type { TextureError } from '../core/TextureError.js'; /** @@ -139,7 +139,7 @@ export type NodeRenderStateEventHandler = ( export interface FpsUpdatePayload { fps: number; contextSpyData: Record | null; - frameCounter: FrameCounter; + frameCount: FrameCount; } /** diff --git a/src/core/Stage.ts b/src/core/Stage.ts index 8cc5fa79..b826c89c 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -71,7 +71,7 @@ export type StageOptions = Omit< textureMemory: TextureMemoryManagerSettings; canvas: HTMLCanvasElement | OffscreenCanvas; fpsUpdateInterval: number; - fpsBuckets?: number[]; + fpsBoundaries?: number[]; eventBus: EventEmitter; platform: Platform | WebPlatform; inspector: boolean; @@ -147,8 +147,6 @@ export class Stage { private timedNodes: CoreNode[] = []; private clrColor = 0x00000000; - private fpsNumFrames = 0; - private fpsElapsedTime = 0; private numQuadsRendered = 0; private renderRequested = false; private frameEventQueue: [name: string, payload: unknown][] = []; @@ -208,8 +206,8 @@ export class Stage { this.contextSpy = enableContextSpy ? new ContextSpy() : null; // Set initial frame buckets and FPS update interval for FPS tracking - if (options.fpsBuckets) { - setFrameBuckets(options.fpsBuckets); + if (options.fpsBoundaries) { + setFrameBuckets(options.fpsBoundaries); } setFpsInterval(options.fpsUpdateInterval); @@ -563,7 +561,13 @@ export class Stage { this.queueFrameEvent('fpsUpdate', { fps: frameCounter.averageFps, contextSpyData: this.contextSpy?.getData() ?? null, - frameCounter: frameCounter, + frameCount: { + //only make a copy of the boundaries array since it's read-only and we want to prevent mutation from outside + boundaries: ([] as number[]).concat(frameCounter.boundaries), + //count and total are not mutated inside the renderer anymore so no copy is necessary + count: frameCounter.count, + total: frameCounter.total, + }, } satisfies FpsUpdatePayload); this.contextSpy?.reset(); frameCounter = this.currentFrameCounter = createFrameCounter(eleapsed); @@ -574,20 +578,6 @@ export class Stage { } frameCounter.increment(this.deltaTime); - // this.fpsNumFrames++; - // this.fpsElapsedTime += this.deltaTime; - // if (this.fpsElapsedTime >= fpsUpdateInterval) { - // const fps = Math.round( - // (this.fpsNumFrames * 1000) / this.fpsElapsedTime, - // ); - // this.fpsNumFrames = 0; - // this.fpsElapsedTime = 0; - // this.queueFrameEvent('fpsUpdate', { - // fps, - // contextSpyData: this.contextSpy?.getData() ?? null, - // } satisfies FpsUpdatePayload); - // this.contextSpy?.reset(); - // } } } diff --git a/src/core/lib/fps.ts b/src/core/lib/fps.ts index 0509fe08..b5d22494 100644 --- a/src/core/lib/fps.ts +++ b/src/core/lib/fps.ts @@ -17,49 +17,70 @@ * limitations under the License. */ -/** - * Class that keeps track of the invocations of Context methods when - * the `enableContextSpy` renderer option is enabled. - */ +export interface FrameCount { + /** + * The boundaries for the frame count buckets. + */ + boundaries: number[]; + /** + * The count of frames in each bucket. + */ + count: Record; + /** + * The total number of frames counted. + */ + total: number; +} -export interface FrameCounter { +export type FrameCounter = FrameCount & { + /** + * The start time of the frame counting interval. + */ start: number; + /** + * The end time of the frame counting interval. + */ end: number; - frameCount: number; - frames: Record; + /** + * Increments the frame count based on the provided frame delta time. + * @param frameDelta - The time in milliseconds it took to render the last frame. + */ increment: (frameDelta: number) => void; + /** + * Calculates and returns the average frames per second (FPS) based on the total frames counted and the elapsed time. + * @returns The average FPS. + */ get averageFps(): number; -} +}; -let buckets = [20, 40, 60, 80, 100]; -let overflowBucketLabel = '>' + buckets[buckets.length - 1]; +let boundaries = [20, 40, 60, 80, 100]; let fpsInterval = 1000; // 1 second const frameCounter: FrameCounter = { start: 0, end: 0, - frameCount: 0, - frames: {}, + total: 0, + boundaries: [], + count: {}, increment(frameDelta: number) { - this.frameCount++; - for (let i = 0; i < buckets.length; i++) { - const bucket = buckets[i] as number; + this.total++; + for (let i = 0; i < boundaries.length; i++) { + const bucket = boundaries[i] as number; if (frameDelta <= bucket) { - this.frames[bucket]!++; + this.count[bucket]!++; return; } } - this.frames[overflowBucketLabel]!++; + this.count['overflow']!++; }, get averageFps() { //calculate fps based on frame count and elapsed time - return (this.frameCount / (this.end - this.start)) * 1000; + return (this.total / (this.end - this.start)) * 1000; }, }; export function setFrameBuckets(newBuckets: number[]) { - buckets = newBuckets; - overflowBucketLabel = '>' + buckets[buckets.length - 1]; + boundaries = newBuckets; } export function setFpsInterval(newInterval: number) { @@ -68,13 +89,14 @@ export function setFpsInterval(newInterval: number) { export function createFrameCounter(frameTime: number): FrameCounter { const counter = Object.create(frameCounter) as FrameCounter; + counter.boundaries = boundaries; counter.start = frameTime; counter.end = frameTime + fpsInterval; //fill frames with 0 for each bucket - for (let i = 0; i < buckets.length; i++) { - const bucket = buckets[i] as number; - counter.frames[bucket] = 0; + for (let i = 0; i < boundaries.length; i++) { + const bucket = boundaries[i] as number; + counter.count[bucket] = 0; } - counter.frames[overflowBucketLabel] = 0; + counter.count['overflow'] = 0; return counter; } From 9ac1f0f831d161be6888791ec7b42968ba0ddcf8 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Thu, 4 Jun 2026 11:32:20 +0200 Subject: [PATCH 3/4] make boundaries and fps interval setable during renderer creation / runtime --- src/core/Stage.ts | 12 ++++++++++++ src/main-api/Renderer.ts | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/core/Stage.ts b/src/core/Stage.ts index b826c89c..25b06ad9 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -581,6 +581,18 @@ export class Stage { } } + updateFpsUpdateInterval(newInterval: number) { + setFpsInterval(newInterval); + // Reset current frame counter to start new interval + this.currentFrameCounter = null; + } + + updateFpsBoundaries(newBoundaries: number[]) { + setFrameBuckets(newBoundaries); + // Reset current frame counter to start new interval with new boundaries + this.currentFrameCounter = null; + } + calculateQuads() { const quads = this.renderer.getQuadCount(); if (quads && quads !== this.numQuadsRendered) { diff --git a/src/main-api/Renderer.ts b/src/main-api/Renderer.ts index 5facc967..e123cf63 100644 --- a/src/main-api/Renderer.ts +++ b/src/main-api/Renderer.ts @@ -238,6 +238,11 @@ export interface RendererRuntimeSettings { */ fpsUpdateInterval: number; + /** + * Boundaries for FPS buckets in the FPS update event payload + */ + fpsBoundaries?: number[]; + /** * Clears the render buffer on reset * @@ -536,6 +541,7 @@ export class RendererMain extends EventEmitter { settings.devicePhysicalPixelRatio || this.windowDevicePixelRatio() || 1, clearColor: settings.clearColor ?? 0x00000000, fpsUpdateInterval: settings.fpsUpdateInterval || 0, + fpsBoundaries: settings.fpsBoundaries, enableClear: settings.enableClear ?? true, targetFPS: settings.targetFPS || 0, numImageWorkers: @@ -599,6 +605,7 @@ export class RendererMain extends EventEmitter { enableContextSpy: settings.enableContextSpy!, forceWebGL2: settings.forceWebGL2!, fpsUpdateInterval: settings.fpsUpdateInterval!, + fpsBoundaries: settings.fpsBoundaries, enableClear: settings.enableClear!, numImageWorkers: settings.numImageWorkers!, renderEngine: settings.renderEngine!, @@ -951,6 +958,14 @@ export class RendererMain extends EventEmitter { needDimensionsUpdate = true; } + if (options.fpsUpdateInterval !== undefined) { + this.stage.updateFpsUpdateInterval(options.fpsUpdateInterval); + } + + if (options.fpsBoundaries !== undefined) { + this.stage.updateFpsBoundaries(options.fpsBoundaries); + } + if (options.boundsMargin !== undefined) { this.stage.setBoundsMargin(options.boundsMargin); } From 07692f52629c729db4ee0d58035149eb9709da2e Mon Sep 17 00:00:00 2001 From: jfboeve Date: Thu, 4 Jun 2026 12:23:22 +0200 Subject: [PATCH 4/4] sort boundaries in ascending order --- src/core/Stage.ts | 6 +++--- src/core/lib/fps.ts | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/core/Stage.ts b/src/core/Stage.ts index 25b06ad9..40e3ec3f 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -60,7 +60,7 @@ import type { RendererMainSettings } from '../main-api/Renderer.js'; import { createFrameCounter, setFpsInterval, - setFrameBuckets, + setFpsBoundaries, type FrameCounter, } from './lib/fps.js'; @@ -207,7 +207,7 @@ export class Stage { // Set initial frame buckets and FPS update interval for FPS tracking if (options.fpsBoundaries) { - setFrameBuckets(options.fpsBoundaries); + setFpsBoundaries(options.fpsBoundaries); } setFpsInterval(options.fpsUpdateInterval); @@ -588,7 +588,7 @@ export class Stage { } updateFpsBoundaries(newBoundaries: number[]) { - setFrameBuckets(newBoundaries); + setFpsBoundaries(newBoundaries); // Reset current frame counter to start new interval with new boundaries this.currentFrameCounter = null; } diff --git a/src/core/lib/fps.ts b/src/core/lib/fps.ts index b5d22494..1c5fb1bf 100644 --- a/src/core/lib/fps.ts +++ b/src/core/lib/fps.ts @@ -79,8 +79,9 @@ const frameCounter: FrameCounter = { }, }; -export function setFrameBuckets(newBuckets: number[]) { - boundaries = newBuckets; +export function setFpsBoundaries(newBoundaries: number[]) { + //sort buckets in ascending order just in case + boundaries = newBoundaries.sort((a, b) => a - b); } export function setFpsInterval(newInterval: number) {