declare global { interface Window { debug: typeof Debug; } } const lessPrecise = (num: number, precision = 5) => parseFloat(num.toPrecision(precision)); const getAvgFrameTime = (times: number[]) => lessPrecise(times.reduce((a, b) => a + b) / times.length); const getFps = (frametime: number) => lessPrecise(1000 / frametime); export class Debug { public static DEBUG_LOG_TIMES = true; private static TIMES_AGGR: Record = {}; private static TIMES_AVG: Record< string, { t: number; times: number[]; avg: number | null } > = {}; private static LAST_DEBUG_LOG_CALL = 0; private static DEBUG_LOG_INTERVAL_ID: null | number = null; private static setupInterval = () => { if (Debug.DEBUG_LOG_INTERVAL_ID === null) { console.info("%c(starting perf recording)", "color: lime"); Debug.DEBUG_LOG_INTERVAL_ID = window.setInterval(Debug.debugLogger, 1000); } Debug.LAST_DEBUG_LOG_CALL = Date.now(); }; private static debugLogger = () => { if ( Date.now() - Debug.LAST_DEBUG_LOG_CALL > 600 && Debug.DEBUG_LOG_INTERVAL_ID !== null ) { window.clearInterval(Debug.DEBUG_LOG_INTERVAL_ID); Debug.DEBUG_LOG_INTERVAL_ID = null; for (const [name, { avg }] of Object.entries(Debug.TIMES_AVG)) { if (avg != null) { console.info( `%c${name} run avg: ${avg}ms (${getFps(avg)} fps)`, "color: blue", ); } } console.info("%c(stopping perf recording)", "color: red"); Debug.TIMES_AGGR = {}; Debug.TIMES_AVG = {}; return; } if (Debug.DEBUG_LOG_TIMES) { for (const [name, { t, times }] of Object.entries(Debug.TIMES_AGGR)) { if (times.length) { console.info( name, lessPrecise(times.reduce((a, b) => a + b)), times.sort((a, b) => a - b).map((x) => lessPrecise(x)), ); Debug.TIMES_AGGR[name] = { t, times: [] }; } } for (const [name, { t, times, avg }] of Object.entries(Debug.TIMES_AVG)) { if (times.length) { const avgFrameTime = getAvgFrameTime(times); console.info(name, `${avgFrameTime}ms (${getFps(avgFrameTime)} fps)`); Debug.TIMES_AVG[name] = { t, times: [], avg: avg != null ? getAvgFrameTime([avg, avgFrameTime]) : avgFrameTime, }; } } } }; public static logTime = (time?: number, name = "default") => { Debug.setupInterval(); const now = performance.now(); const { t, times } = (Debug.TIMES_AGGR[name] = Debug.TIMES_AGGR[name] || { t: 0, times: [], }); if (t) { times.push(time != null ? time : now - t); } Debug.TIMES_AGGR[name].t = now; }; public static logTimeAverage = (time?: number, name = "default") => { Debug.setupInterval(); const now = performance.now(); const { t, times } = (Debug.TIMES_AVG[name] = Debug.TIMES_AVG[name] || { t: 0, times: [], }); if (t) { times.push(time != null ? time : now - t); } Debug.TIMES_AVG[name].t = now; }; private static logWrapper = (type: "logTime" | "logTimeAverage") => (fn: (...args: T) => R, name = "default") => { return (...args: T) => { const t0 = performance.now(); const ret = fn(...args); Debug.logTime(performance.now() - t0, name); return ret; }; }; public static logTimeWrap = Debug.logWrapper("logTime"); public static logTimeAverageWrap = Debug.logWrapper("logTimeAverage"); public static perfWrap = ( fn: (...args: T) => R, name = "default", ) => { return (...args: T) => { // eslint-disable-next-line no-console console.time(name); const ret = fn(...args); // eslint-disable-next-line no-console console.timeEnd(name); return ret; }; }; } //@ts-ignore window.debug = Debug;