| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
|
|
|
|
| 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),
|
| );
|
| }
|
| }
|
|
|