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