summit/frontend/node_modules/stats-gl/lib/main.ts

501 lines
12 KiB
TypeScript

import * as THREE from 'three';
import { Panel } from './panel';
interface StatsOptions {
trackGPU?: boolean;
logsPerSecond?: number;
samplesLog?: number;
samplesGraph?: number;
precision?: number;
minimal?: boolean;
horizontal?: boolean;
mode?: number;
}
interface QueryInfo {
query: WebGLQuery;
}
interface AverageData {
logs: number[];
graph: number[];
}
interface InfoData {
render: {
timestamp: number;
};
compute: {
timestamp: number;
};
}
class Stats {
private dom: HTMLDivElement;
private mode: number;
private horizontal: boolean;
private minimal: boolean;
private trackGPU: boolean;
private samplesLog: number;
private samplesGraph: number;
private precision: number;
private logsPerSecond: number;
private gl: WebGL2RenderingContext | null = null;
private ext: any | null = null;
private info?: InfoData;
private activeQuery: WebGLQuery | null = null;
private gpuQueries: QueryInfo[] = [];
private threeRendererPatched = false;
private beginTime: number;
private prevTime: number;
private prevCpuTime: number;
private frames = 0;
private renderCount = 0;
private isRunningCPUProfiling = false;
private totalCpuDuration = 0;
private totalGpuDuration = 0;
private totalGpuDurationCompute = 0;
private totalFps = 0;
private fpsPanel: Panel;
private msPanel: Panel;
private gpuPanel: Panel | null = null;
private gpuPanelCompute: Panel | null = null;
private averageFps: AverageData = { logs: [], graph: [] };
private averageCpu: AverageData = { logs: [], graph: [] };
private averageGpu: AverageData = { logs: [], graph: [] };
private averageGpuCompute: AverageData = { logs: [], graph: [] };
static Panel = Panel;
constructor({
trackGPU = false,
logsPerSecond = 30,
samplesLog = 60,
samplesGraph = 10,
precision = 2,
minimal = false,
horizontal = true,
mode = 0
}: StatsOptions = {}) {
this.mode = mode;
this.horizontal = horizontal;
this.minimal = minimal;
this.trackGPU = trackGPU;
this.samplesLog = samplesLog;
this.samplesGraph = samplesGraph;
this.precision = precision;
this.logsPerSecond = logsPerSecond;
// Initialize DOM
this.dom = document.createElement('div');
this.initializeDOM();
// Initialize timing
this.beginTime = performance.now();
this.prevTime = this.beginTime;
this.prevCpuTime = this.beginTime;
// Create panels
this.fpsPanel = this.addPanel(new Stats.Panel('FPS', '#0ff', '#002'), 0);
this.msPanel = this.addPanel(new Stats.Panel('CPU', '#0f0', '#020'), 1);
this.setupEventListeners();
}
private initializeDOM(): void {
this.dom.style.cssText = `
position: fixed;
top: 0;
left: 0;
opacity: 0.9;
z-index: 10000;
${this.minimal ? 'cursor: pointer;' : ''}
`;
}
private setupEventListeners(): void {
if (this.minimal) {
this.dom.addEventListener('click', this.handleClick);
this.showPanel(this.mode);
} else {
window.addEventListener('resize', this.handleResize);
}
}
private handleClick = (event: MouseEvent): void => {
event.preventDefault();
this.showPanel(++this.mode % this.dom.children.length);
};
private handleResize = (): void => {
this.resizePanel(this.fpsPanel, 0);
this.resizePanel(this.msPanel, 1);
if (this.gpuPanel) this.resizePanel(this.gpuPanel, 2);
if (this.gpuPanelCompute) this.resizePanel(this.gpuPanelCompute, 3);
};
public async init(
canvasOrGL: WebGL2RenderingContext | HTMLCanvasElement | OffscreenCanvas | any
): Promise<void> {
if (!canvasOrGL) {
console.error('Stats: The "canvas" parameter is undefined.');
return;
}
if (this.handleThreeRenderer(canvasOrGL)) return;
if (await this.handleWebGPURenderer(canvasOrGL)) return;
if (!this.initializeWebGL(canvasOrGL)) return;
}
private handleThreeRenderer(renderer: any): boolean {
if (renderer.isWebGLRenderer && !this.threeRendererPatched) {
this.patchThreeRenderer(renderer);
this.gl = renderer.getContext();
if (this.trackGPU) {
this.initializeGPUTracking();
}
return true;
}
return false;
}
private async handleWebGPURenderer(renderer: any): Promise<boolean> {
if (renderer.isWebGPURenderer) {
if (this.trackGPU) {
renderer.backend.trackTimestamp = true;
if (await renderer.hasFeatureAsync('timestamp-query')) {
this.initializeWebGPUPanels();
}
}
this.info = renderer.info;
return true;
}
return false;
}
private initializeWebGPUPanels(): void {
this.gpuPanel = this.addPanel(new Stats.Panel('GPU', '#ff0', '#220'), 2);
this.gpuPanelCompute = this.addPanel(
new Stats.Panel('CPT', '#e1e1e1', '#212121'),
3
);
}
private initializeWebGL(
canvasOrGL: WebGL2RenderingContext | HTMLCanvasElement | OffscreenCanvas
): boolean {
if (canvasOrGL instanceof WebGL2RenderingContext) {
this.gl = canvasOrGL;
} else if (
canvasOrGL instanceof HTMLCanvasElement ||
canvasOrGL instanceof OffscreenCanvas
) {
this.gl = canvasOrGL.getContext('webgl2');
if (!this.gl) {
console.error('Stats: Unable to obtain WebGL2 context.');
return false;
}
} else {
console.error(
'Stats: Invalid input type. Expected WebGL2RenderingContext, HTMLCanvasElement, or OffscreenCanvas.'
);
return false;
}
return true;
}
private initializeGPUTracking(): void {
if (this.gl) {
this.ext = this.gl.getExtension('EXT_disjoint_timer_query_webgl2');
if (this.ext) {
this.gpuPanel = this.addPanel(new Stats.Panel('GPU', '#ff0', '#220'), 2);
}
}
}
public begin(): void {
if (!this.isRunningCPUProfiling) {
this.beginProfiling('cpu-started');
}
if (!this.gl || !this.ext) return;
if (this.activeQuery) {
this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);
}
this.activeQuery = this.gl.createQuery();
if (this.activeQuery) {
this.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, this.activeQuery);
}
}
public end(): void {
this.renderCount++;
if (this.gl && this.ext && this.activeQuery) {
this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);
this.gpuQueries.push({ query: this.activeQuery });
this.activeQuery = null;
}
}
public update(): void {
if (!this.info) {
this.processGpuQueries();
} else {
this.processWebGPUTimestamps();
}
this.endProfiling('cpu-started', 'cpu-finished', 'cpu-duration');
this.updateAverages();
this.resetCounters();
}
private processWebGPUTimestamps(): void {
this.totalGpuDuration = this.info!.render.timestamp;
this.totalGpuDurationCompute = this.info!.compute.timestamp;
this.addToAverage(this.totalGpuDurationCompute, this.averageGpuCompute);
}
private updateAverages(): void {
this.addToAverage(this.totalCpuDuration, this.averageCpu);
this.addToAverage(this.totalGpuDuration, this.averageGpu);
}
private resetCounters(): void {
this.renderCount = 0;
if (this.totalCpuDuration === 0) {
this.beginProfiling('cpu-started');
}
this.totalCpuDuration = 0;
this.totalFps = 0;
this.beginTime = this.endInternal();
}
resizePanel(panel: Panel, offset: number) {
panel.canvas.style.position = 'absolute';
if (this.minimal) {
panel.canvas.style.display = 'none';
} else {
panel.canvas.style.display = 'block';
if (this.horizontal) {
panel.canvas.style.top = '0px';
panel.canvas.style.left = offset * panel.WIDTH / panel.PR + 'px';
} else {
panel.canvas.style.left = '0px';
panel.canvas.style.top = offset * panel.HEIGHT / panel.PR + 'px';
}
}
}
addPanel(panel: Panel, offset: number) {
if (panel.canvas) {
this.dom.appendChild(panel.canvas);
this.resizePanel(panel, offset);
}
return panel;
}
showPanel(id: number) {
for (let i = 0; i < this.dom.children.length; i++) {
const child = this.dom.children[i] as HTMLElement;
child.style.display = i === id ? 'block' : 'none';
}
this.mode = id;
}
processGpuQueries() {
if (!this.gl || !this.ext) return;
this.totalGpuDuration = 0;
this.gpuQueries.forEach((queryInfo, index) => {
if (this.gl) {
const available = this.gl.getQueryParameter(queryInfo.query, this.gl.QUERY_RESULT_AVAILABLE);
const disjoint = this.gl.getParameter(this.ext.GPU_DISJOINT_EXT);
if (available && !disjoint) {
const elapsed = this.gl.getQueryParameter(queryInfo.query, this.gl.QUERY_RESULT);
const duration = elapsed * 1e-6; // Convert nanoseconds to milliseconds
this.totalGpuDuration += duration;
this.gl.deleteQuery(queryInfo.query);
this.gpuQueries.splice(index, 1); // Remove the processed query
}
}
});
}
endInternal() {
this.frames++;
const time = (performance || Date).now();
const elapsed = time - this.prevTime;
// Calculate FPS more frequently based on logsPerSecond
if (time >= this.prevCpuTime + 1000 / this.logsPerSecond) {
// Calculate FPS and round to nearest integer
const fps = Math.round((this.frames * 1000) / elapsed);
// Add to FPS averages
this.addToAverage(fps, this.averageFps);
// Update all panels
this.updatePanel(this.fpsPanel, this.averageFps, 0);
this.updatePanel(this.msPanel, this.averageCpu, this.precision);
this.updatePanel(this.gpuPanel, this.averageGpu, this.precision);
if (this.gpuPanelCompute) {
this.updatePanel(this.gpuPanelCompute, this.averageGpuCompute);
}
// Reset frame counter for next interval
this.frames = 0;
this.prevCpuTime = time;
this.prevTime = time;
}
return time;
}
addToAverage(value: number, averageArray: { logs: any; graph: any; }) {
averageArray.logs.push(value);
if (averageArray.logs.length > this.samplesLog) {
averageArray.logs.shift();
}
averageArray.graph.push(value);
if (averageArray.graph.length > this.samplesGraph) {
averageArray.graph.shift();
}
}
beginProfiling(marker: string) {
if (window.performance) {
window.performance.mark(marker);
this.isRunningCPUProfiling = true
}
}
endProfiling(startMarker: string | PerformanceMeasureOptions | undefined, endMarker: string | undefined, measureName: string) {
if (window.performance && endMarker && this.isRunningCPUProfiling) {
window.performance.mark(endMarker);
const cpuMeasure = performance.measure(measureName, startMarker, endMarker);
this.totalCpuDuration += cpuMeasure.duration;
this.isRunningCPUProfiling = false
}
}
updatePanel(panel: { update: any; } | null, averageArray: { logs: number[], graph: number[] }, precision = 2) {
if (averageArray.logs.length > 0) {
let sumLog = 0;
let max = 0.01;
for (let i = 0; i < averageArray.logs.length; i++) {
sumLog += averageArray.logs[i];
if (averageArray.logs[i] > max) {
max = averageArray.logs[i];
}
}
let sumGraph = 0;
let maxGraph = 0.01;
for (let i = 0; i < averageArray.graph.length; i++) {
sumGraph += averageArray.graph[i];
if (averageArray.graph[i] > maxGraph) {
maxGraph = averageArray.graph[i];
}
}
if (panel) {
panel.update(sumLog / Math.min(averageArray.logs.length, this.samplesLog), sumGraph / Math.min(averageArray.graph.length, this.samplesGraph), max, maxGraph, precision);
}
}
}
get domElement() {
// patch for some use case in threejs
return this.dom;
}
patchThreeRenderer(renderer: any) {
// Store the original render method
const originalRenderMethod = renderer.render;
// Reference to the stats instance
const statsInstance = this;
// Override the render method on the prototype
renderer.render = function (scene: THREE.Scene, camera: THREE.Camera) {
statsInstance.begin(); // Start tracking for this render call
// Call the original render method
originalRenderMethod.call(this, scene, camera);
statsInstance.end(); // End tracking for this render call
};
this.threeRendererPatched = true;
}
}
export default Stats;