Spaces:
Sleeping
Sleeping
| /** | |
| * Copyright (c) Meta Platforms, Inc. and affiliates. | |
| * | |
| * 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. | |
| */ | |
| /** | |
| * Derived from mrdoob / http://mrdoob.com/ | |
| */ | |
| import Logger from '@/common/logger/Logger'; | |
| import {uuidv4} from '@/common/utils/uuid'; | |
| import invariant from 'invariant'; | |
| export type Request<A, P> = { | |
| action: A; | |
| } & P; | |
| export type Response<A, P> = Request<A, P>; | |
| export type GetStatsCanvasRequest = Request< | |
| 'getStatsCanvas', | |
| { | |
| id: string; | |
| width: number; | |
| height: number; | |
| } | |
| >; | |
| export type GetMemoryStatsRequest = Request< | |
| 'getMemoryStats', | |
| { | |
| id: string; | |
| jsHeapSizeLimit: number; | |
| totalJSHeapSize: number; | |
| usedJSHeapSize: number; | |
| } | |
| >; | |
| export type SetStatsCanvasResponse = Response< | |
| 'setStatsCanvas', | |
| { | |
| id: string; | |
| canvas: OffscreenCanvas; | |
| devicePixelRatio: number; | |
| } | |
| >; | |
| export type MemoryStatsResponse = Response< | |
| 'memoryStats', | |
| { | |
| id: string; | |
| jsHeapSizeLimit: number; | |
| totalJSHeapSize: number; | |
| usedJSHeapSize: number; | |
| } | |
| >; | |
| export type StatsType = 'fps' | 'ms' | 'memory'; | |
| export class Stats { | |
| private maxValue: number; | |
| private beginTime: number; | |
| private prevTime: number; | |
| private frames: number; | |
| private fpsPanel: Panel | null = null; | |
| private msPanel: Panel | null = null; | |
| private memPanel: Panel | null = null; | |
| constructor(type: StatsType, label: string = '', maxValue: number = 100) { | |
| const id = uuidv4(); | |
| this.maxValue = maxValue; | |
| this.beginTime = (performance || Date).now(); | |
| this.prevTime = this.beginTime; | |
| this.frames = 0; | |
| const onMessage = (event: MessageEvent<SetStatsCanvasResponse>) => { | |
| if (event.data.action === 'setStatsCanvas' && event.data.id === id) { | |
| const {canvas, devicePixelRatio} = event.data; | |
| if (type === 'fps') { | |
| this.fpsPanel = new Panel( | |
| canvas, | |
| devicePixelRatio, | |
| `FPS ${label}`.trim(), | |
| '#0ff', | |
| '#002', | |
| ); | |
| } else if (type === 'ms') { | |
| this.msPanel = new Panel( | |
| canvas, | |
| devicePixelRatio, | |
| `MS ${label}`.trim(), | |
| '#0f0', | |
| '#020', | |
| ); | |
| } else if (type === 'memory') { | |
| this.memPanel = new Panel( | |
| canvas, | |
| devicePixelRatio, | |
| `MB ${label}`.trim(), | |
| '#f08', | |
| '#201', | |
| ); | |
| } | |
| self.removeEventListener('message', onMessage); | |
| } | |
| }; | |
| self.addEventListener('message', onMessage); | |
| self.postMessage({ | |
| action: 'getStatsCanvas', | |
| id, | |
| width: 80, | |
| height: 48, | |
| } as GetStatsCanvasRequest); | |
| } | |
| updateMaxValue(maxValue: number) { | |
| this.maxValue = maxValue; | |
| } | |
| begin() { | |
| this.beginTime = (performance || Date).now(); | |
| } | |
| end() { | |
| this.frames++; | |
| const time = (performance || Date).now(); | |
| this.msPanel?.update(time - this.beginTime, this.maxValue); | |
| if (time >= this.prevTime + 1000) { | |
| this.fpsPanel?.update( | |
| (this.frames * 1000) / (time - this.prevTime), | |
| this.maxValue, | |
| ); | |
| this.prevTime = time; | |
| this.frames = 0; | |
| const id = uuidv4(); | |
| const onMessage = (event: MessageEvent<MemoryStatsResponse>) => { | |
| if (event.data.action === 'memoryStats' && event.data.id === id) { | |
| const {usedJSHeapSize, jsHeapSizeLimit} = event.data; | |
| this.memPanel?.update( | |
| usedJSHeapSize / 1048576, | |
| jsHeapSizeLimit / 1048576, | |
| ); | |
| } | |
| }; | |
| self.addEventListener('message', onMessage); | |
| self.postMessage({ | |
| action: 'getMemoryStats', | |
| id, | |
| } as GetMemoryStatsRequest); | |
| } | |
| return time; | |
| } | |
| update() { | |
| this.beginTime = this.end(); | |
| } | |
| } | |
| export class Panel { | |
| private min = Infinity; | |
| private max = 0; | |
| private round = Math.round; | |
| private PR: number; | |
| private WIDTH: number; | |
| private HEIGHT: number; | |
| private TEXT_X: number; | |
| private TEXT_Y: number; | |
| private GRAPH_X: number; | |
| private GRAPH_Y: number; | |
| private GRAPH_WIDTH: number; | |
| private GRAPH_HEIGHT: number; | |
| public canvas: HTMLCanvasElement | OffscreenCanvas; | |
| private context: | |
| | CanvasRenderingContext2D | |
| | OffscreenCanvasRenderingContext2D | |
| | null = null; | |
| private name: string; | |
| private fg: string; | |
| private bg: string; | |
| constructor( | |
| canvas: HTMLCanvasElement | OffscreenCanvas, | |
| devicePixelRatio: number, | |
| name: string, | |
| fg: string, | |
| bg: string, | |
| ) { | |
| this.canvas = canvas; | |
| this.name = name; | |
| this.fg = fg; | |
| this.bg = bg; | |
| this.PR = this.round(devicePixelRatio || 1); | |
| this.WIDTH = 80 * this.PR; | |
| this.HEIGHT = 48 * this.PR; | |
| this.TEXT_X = 3 * this.PR; | |
| this.TEXT_Y = 2 * this.PR; | |
| this.GRAPH_X = 3 * this.PR; | |
| this.GRAPH_Y = 15 * this.PR; | |
| this.GRAPH_WIDTH = 74 * this.PR; | |
| this.GRAPH_HEIGHT = 30 * this.PR; | |
| const context: OffscreenCanvasRenderingContext2D | RenderingContext | null = | |
| canvas.getContext('2d'); | |
| invariant(context !== null, 'context 2d is required'); | |
| if ( | |
| !(context instanceof CanvasRenderingContext2D) && | |
| !(context instanceof OffscreenCanvasRenderingContext2D) | |
| ) { | |
| Logger.warn( | |
| 'rendering stats requires CanvasRenderingContext2D or OffscreenCanvasRenderingContext2D', | |
| ); | |
| return; | |
| } | |
| context.font = 'bold ' + 9 * this.PR + 'px Helvetica,Arial,sans-serif'; | |
| context.textBaseline = 'top'; | |
| context.fillStyle = bg; | |
| context.fillRect(0, 0, this.WIDTH, this.HEIGHT); | |
| context.fillStyle = fg; | |
| context.fillText(name, this.TEXT_X, this.TEXT_Y); | |
| context.fillRect( | |
| this.GRAPH_X, | |
| this.GRAPH_Y, | |
| this.GRAPH_WIDTH, | |
| this.GRAPH_HEIGHT, | |
| ); | |
| context.fillStyle = bg; | |
| context.globalAlpha = 0.9; | |
| context.fillRect( | |
| this.GRAPH_X, | |
| this.GRAPH_Y, | |
| this.GRAPH_WIDTH, | |
| this.GRAPH_HEIGHT, | |
| ); | |
| this.context = context; | |
| } | |
| update(value: number, maxValue: number) { | |
| invariant(this.context !== null, 'context 2d is required'); | |
| this.min = Math.min(this.min, value); | |
| this.max = Math.max(this.max, value); | |
| this.context.fillStyle = this.bg; | |
| this.context.globalAlpha = 1; | |
| this.context.fillRect(0, 0, this.WIDTH, this.GRAPH_Y); | |
| this.context.fillStyle = this.fg; | |
| this.context.fillText( | |
| this.round(value) + | |
| ' ' + | |
| this.name + | |
| ' (' + | |
| this.round(this.min) + | |
| '-' + | |
| this.round(this.max) + | |
| ')', | |
| this.TEXT_X, | |
| this.TEXT_Y, | |
| ); | |
| this.context.drawImage( | |
| this.canvas, | |
| this.GRAPH_X + this.PR, | |
| this.GRAPH_Y, | |
| this.GRAPH_WIDTH - this.PR, | |
| this.GRAPH_HEIGHT, | |
| this.GRAPH_X, | |
| this.GRAPH_Y, | |
| this.GRAPH_WIDTH - this.PR, | |
| this.GRAPH_HEIGHT, | |
| ); | |
| this.context.fillRect( | |
| this.GRAPH_X + this.GRAPH_WIDTH - this.PR, | |
| this.GRAPH_Y, | |
| this.PR, | |
| this.GRAPH_HEIGHT, | |
| ); | |
| this.context.fillStyle = this.bg; | |
| this.context.globalAlpha = 0.9; | |
| this.context.fillRect( | |
| this.GRAPH_X + this.GRAPH_WIDTH - this.PR, | |
| this.GRAPH_Y, | |
| this.PR, | |
| this.round((1 - value / maxValue) * this.GRAPH_HEIGHT), | |
| ); | |
| } | |
| } | |