Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/common/CommonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import type { CoreNodeRenderState } from '../core/CoreNode.js';
import type { FrameCount } from '../core/lib/fps.js';
import type { TextureError } from '../core/TextureError.js';

/**
Expand Down Expand Up @@ -138,6 +139,7 @@ export type NodeRenderStateEventHandler = (
export interface FpsUpdatePayload {
fps: number;
contextSpyData: Record<string, number> | null;
frameCount: FrameCount;
}
Comment on lines 139 to 143

/**
Expand Down
63 changes: 48 additions & 15 deletions src/core/Stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
setFpsBoundaries,
type FrameCounter,
} from './lib/fps.js';

export type StageOptions = Omit<
RendererMainSettings,
Expand All @@ -65,6 +71,7 @@ export type StageOptions = Omit<
textureMemory: TextureMemoryManagerSettings;
canvas: HTMLCanvasElement | OffscreenCanvas;
fpsUpdateInterval: number;
fpsBoundaries?: number[];
eventBus: EventEmitter;
platform: Platform | WebPlatform;
inspector: boolean;
Expand Down Expand Up @@ -136,10 +143,10 @@ export class Stage {
lastFrameTime = 0;
currentFrameTime = 0;
elapsedTime = 0;
currentFrameCounter: FrameCounter | null = null;

private timedNodes: CoreNode[] = [];
private clrColor = 0x00000000;
private fpsNumFrames = 0;
private fpsElapsedTime = 0;
private numQuadsRendered = 0;
private renderRequested = false;
private frameEventQueue: [name: string, payload: unknown][] = [];
Expand Down Expand Up @@ -198,6 +205,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.fpsBoundaries) {
setFpsBoundaries(options.fpsBoundaries);
}
setFpsInterval(options.fpsUpdateInterval);
Comment on lines +208 to +212

let bm = [0, 0, 0, 0] as [number, number, number, number];
if (boundsMargin) {
bm = Array.isArray(boundsMargin)
Expand Down Expand Up @@ -382,9 +395,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;

Expand Down Expand Up @@ -542,24 +554,45 @@ 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) {
Comment on lines +558 to +560
this.queueFrameEvent('fpsUpdate', {
Comment on lines +557 to 561
fps,
fps: frameCounter.averageFps,
contextSpyData: this.contextSpy?.getData() ?? null,
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);
}

if (frameCounter === null) {
frameCounter = this.currentFrameCounter = createFrameCounter(eleapsed);
}

frameCounter.increment(this.deltaTime);
}
}

updateFpsUpdateInterval(newInterval: number) {
setFpsInterval(newInterval);
// Reset current frame counter to start new interval
this.currentFrameCounter = null;
}

updateFpsBoundaries(newBoundaries: number[]) {
setFpsBoundaries(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) {
Expand Down
103 changes: 103 additions & 0 deletions src/core/lib/fps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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.
*/

export interface FrameCount {
/**
* The boundaries for the frame count buckets.
*/
boundaries: number[];
/**
* The count of frames in each bucket.
*/
count: Record<number | string, number>;
/**
* The total number of frames counted.
*/
total: number;
}
Comment thread
jfboeve marked this conversation as resolved.

export type FrameCounter = FrameCount & {
/**
* The start time of the frame counting interval.
*/
start: number;
/**
* The end time of the frame counting interval.
*/
end: number;
/**
* 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 boundaries = [20, 40, 60, 80, 100];
let fpsInterval = 1000; // 1 second

const frameCounter: FrameCounter = {
start: 0,
end: 0,
total: 0,
boundaries: [],
count: {},
increment(frameDelta: number) {
this.total++;
for (let i = 0; i < boundaries.length; i++) {
const bucket = boundaries[i] as number;
if (frameDelta <= bucket) {
this.count[bucket]!++;
return;
}
}
this.count['overflow']!++;
},
Comment on lines +65 to +75
get averageFps() {
//calculate fps based on frame count and elapsed time
return (this.total / (this.end - this.start)) * 1000;
},
};
Comment on lines +56 to +80

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) {
fpsInterval = newInterval;
}

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 < boundaries.length; i++) {
const bucket = boundaries[i] as number;
counter.count[bucket] = 0;
}
Comment on lines +91 to +100
counter.count['overflow'] = 0;
return counter;
}
15 changes: 15 additions & 0 deletions src/main-api/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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!,
Expand Down Expand Up @@ -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);
}
Expand Down
Loading