From 356cf008ced9551de625910ab227fe61d8df2f55 Mon Sep 17 00:00:00 2001 From: Alexander Rose <alexander.rose@weirdbyte.de> Date: Sat, 4 Feb 2023 19:25:08 -0800 Subject: [PATCH] add consoleStats, improve timer --- src/apps/viewer/app.ts | 4 +- src/examples/alpha-orbitals/index.ts | 5 +- src/mol-canvas3d/canvas3d.ts | 12 ++- src/mol-canvas3d/passes/pick.ts | 2 +- src/mol-gl/webgl/context.ts | 10 +- src/mol-gl/webgl/render-item.ts | 8 +- src/mol-gl/webgl/timer.ts | 148 ++++++++++++++++++++++++--- src/mol-util/debug.ts | 22 +++- 8 files changed, 184 insertions(+), 27 deletions(-) diff --git a/src/apps/viewer/app.ts b/src/apps/viewer/app.ts index c6a531763..47656082c 100644 --- a/src/apps/viewer/app.ts +++ b/src/apps/viewer/app.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -50,7 +50,7 @@ import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/car import { Backgrounds } from '../../extensions/backgrounds'; export { PLUGIN_VERSION as version } from '../../mol-plugin/version'; -export { setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug'; +export { setDebugMode, setProductionMode, setTimingMode, consoleStats } from '../../mol-util/debug'; const CustomFormats = [ ['g3d', G3dProvider] as const diff --git a/src/examples/alpha-orbitals/index.ts b/src/examples/alpha-orbitals/index.ts index cf463bfee..05c52c4eb 100644 --- a/src/examples/alpha-orbitals/index.ts +++ b/src/examples/alpha-orbitals/index.ts @@ -25,7 +25,7 @@ import { DemoMoleculeSDF, DemoOrbitals } from './example-data'; import './index.html'; require('mol-plugin-ui/skin/light.scss'); -import { setDebugMode, setTimingMode } from '../../mol-util/debug'; +import { setDebugMode, setTimingMode, consoleStats } from '../../mol-util/debug'; interface DemoInput { moleculeSdf: string, @@ -222,4 +222,5 @@ export class AlphaOrbitalsExample { (window as any).AlphaOrbitalsExample = new AlphaOrbitalsExample(); (window as any).AlphaOrbitalsExample.setDebugMode = setDebugMode; -(window as any).AlphaOrbitalsExample.setTimingMode = setTimingMode; \ No newline at end of file +(window as any).AlphaOrbitalsExample.setTimingMode = setTimingMode; +(window as any).AlphaOrbitalsExample.consoleStats = consoleStats; \ No newline at end of file diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index b65f42eb7..969831e70 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author David Sehnal <david.sehnal@gmail.com> @@ -31,7 +31,7 @@ import { PickData } from './passes/pick'; import { PickHelper } from './passes/pick'; import { ImagePass, ImageProps } from './passes/image'; import { Sphere3D } from '../mol-math/geometry'; -import { isDebugMode, isTimingMode } from '../mol-util/debug'; +import { addConsoleStatsProvider, isDebugMode, isTimingMode, removeConsoleStatsProvider } from '../mol-util/debug'; import { CameraHelperParams } from './helper/camera-helper'; import { produce } from 'immer'; import { HandleHelperParams } from './helper/handle-helper'; @@ -440,7 +440,7 @@ namespace Canvas3D { cam = stereoCamera; } - if (isTimingMode) webgl.timer.mark('Canvas3D.render'); + if (isTimingMode) webgl.timer.mark('Canvas3D.render', true); const ctx = { renderer, camera: cam, scene, helper }; if (MultiSamplePass.isEnabled(p.multiSample)) { const forceOn = !cameraChanged && markingUpdated && !controls.isAnimating; @@ -623,6 +623,8 @@ namespace Canvas3D { attribute: `${(attribute / 1024 / 1024).toFixed(3)} MiB`, elements: `${(elements / 1024 / 1024).toFixed(3)} MiB`, }); + + console.log(webgl.timer.formatedStats()); } function add(repr: Representation.Any) { @@ -728,6 +730,8 @@ namespace Canvas3D { resized.next(+new Date()); } + addConsoleStatsProvider(consoleStats); + return { webgl, @@ -905,6 +909,8 @@ namespace Canvas3D { controls.dispose(); renderer.dispose(); interactionHelper.dispose(); + + removeConsoleStatsProvider(consoleStats); } }; diff --git a/src/mol-canvas3d/passes/pick.ts b/src/mol-canvas3d/passes/pick.ts index 83199e143..8df182f66 100644 --- a/src/mol-canvas3d/passes/pick.ts +++ b/src/mol-canvas3d/passes/pick.ts @@ -291,7 +291,7 @@ export class PickHelper { } private render(camera: Camera | StereoCamera) { - if (isTimingMode) this.webgl.timer.mark('PickHelper.render'); + if (isTimingMode) this.webgl.timer.mark('PickHelper.render', true); const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this; const { renderer, scene, helper } = this; diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts index c638f03fb..83d78ff21 100644 --- a/src/mol-gl/webgl/context.ts +++ b/src/mol-gl/webgl/context.ts @@ -155,7 +155,7 @@ function getDrawingBufferPixelData(gl: GLRenderingContext, state: WebGLState) { // function createStats() { - return { + const stats = { resourceCounts: { attribute: 0, elements: 0, @@ -171,7 +171,13 @@ function createStats() { drawCount: 0, instanceCount: 0, instancedDrawCount: 0, + + calls: { + drawInstanced: 0, + counts: 0, + }, }; + return stats; } export type WebGLStats = ReturnType<typeof createStats> @@ -224,7 +230,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal const state = createState(gl); const stats = createStats(); const resources = createResources(gl, state, stats, extensions); - const timer = createTimer(gl, extensions); + const timer = createTimer(gl, extensions, stats); const parameters = { maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number, diff --git a/src/mol-gl/webgl/render-item.ts b/src/mol-gl/webgl/render-item.ts index 47d62df93..b3d74edc8 100644 --- a/src/mol-gl/webgl/render-item.ts +++ b/src/mol-gl/webgl/render-item.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Gianluca Tomasello <giagitom@gmail.com> @@ -15,7 +15,7 @@ import { idFactory } from '../../mol-util/id-factory'; import { ValueCell } from '../../mol-util'; import { TextureImage, TextureVolume } from '../../mol-gl/renderable/util'; import { checkFramebufferStatus } from './framebuffer'; -import { isDebugMode } from '../../mol-util/debug'; +import { isDebugMode, isTimingMode } from '../../mol-util/debug'; import { VertexArray } from './vertex-array'; import { fillSerial } from '../../mol-util/array'; import { deepClone } from '../../mol-util/object'; @@ -212,6 +212,10 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode: } else { instancedArrays.drawArraysInstanced(glDrawMode, 0, drawCount, instanceCount); } + if (isTimingMode) { + stats.calls.drawInstanced += 1; + stats.calls.counts += instanceCount; + } if (isDebugMode) { try { checkError(ctx.gl); diff --git a/src/mol-gl/webgl/timer.ts b/src/mol-gl/webgl/timer.ts index 2479455bf..9678ddf07 100644 --- a/src/mol-gl/webgl/timer.ts +++ b/src/mol-gl/webgl/timer.ts @@ -1,16 +1,54 @@ /** - * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ +import { now } from '../../mol-util/now'; import { GLRenderingContext } from './compat'; +import { WebGLStats } from './context'; import { WebGLExtensions } from './extensions'; +function movingAverage(avg: number, sample: number, count: number) { + avg -= avg / count; + avg += sample / count; + return avg; +} + +class MovingAverage { + private readonly avgs = new Map<string, number>(); + + add(label: string, sample: number) { + let avg = this.avgs.get(label) || sample; + avg = movingAverage(avg, sample, this.count); + this.avgs.set(label, avg); + return avg; + } + + get(label: string) { + return this.avgs.get(label); + } + + stats() { + return Object.fromEntries(this.avgs.entries()); + } + + constructor(private count: number) { } +} + +function clearStatsCalls(stats: WebGLStats) { + stats.calls.drawInstanced = 0; + stats.calls.counts = 0; +} + export type TimerResult = { readonly label: string - readonly timeElapsed: number + readonly gpuElapsed: number + readonly gpuAvg: number + readonly cpuElapsed: number + readonly cpuAvg: number readonly children: TimerResult[] + readonly calls?: Calls } function getQuery(extensions: WebGLExtensions) { @@ -20,24 +58,45 @@ function getQuery(extensions: WebGLExtensions) { export type WebGLTimer = { /** Check with GPU for finished timers. */ resolve: () => TimerResult[] - mark: (label: string) => void + mark: (label: string, captureCalls?: boolean) => void markEnd: (label: string) => void + stats: () => { gpu: Record<string, number>, cpu: Record<string, number> } + formatedStats: () => Record<string, string> clear: () => void destroy: () => void } -type Measure = { label: string, queries: WebGLQuery[], children: Measure[], root: boolean, timeElapsed?: number }; +type Calls = { + drawInstanced: number, + counts: number, +} + +type Measure = { + label: string, + queries: WebGLQuery[], + children: Measure[], + root: boolean, + cpu: { start: number, end: number }, + captureCalls: boolean, + timeElapsed?: number, + calls?: Calls, +}; + type QueryResult = { timeElapsed?: number, refCount: number }; -export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions): WebGLTimer { +export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions, stats: WebGLStats, options?: { avgCount: number }): WebGLTimer { const dtq = extensions.disjointTimerQuery; + const avgCount = options?.avgCount ?? 30; const queries = new Map<WebGLQuery, QueryResult>(); const pending = new Map<string, Measure>(); const stack: Measure[] = []; + const gpuAvgs = new MovingAverage(avgCount); + const cpuAvgs = new MovingAverage(avgCount); let measures: Measure[] = []; let current: WebGLQuery | null = null; + let capturingCalls = false; const clear = () => { if (!dtq) return; @@ -100,17 +159,32 @@ export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions) const children: TimerResult[] = []; const add = (measures: Measure[], children: TimerResult[]) => { for (const measure of measures) { + const timeElapsed = measure.timeElapsed!; + const cpuElapsed = measure.cpu.end - measure.cpu.start; const result: TimerResult = { label: measure.label, - timeElapsed: measure.timeElapsed!, - children: [] + gpuElapsed: timeElapsed, + gpuAvg: gpuAvgs.add(measure.label, timeElapsed), + cpuElapsed, + cpuAvg: cpuAvgs.add(measure.label, cpuElapsed), + children: [], + calls: measure.calls, }; children.push(result); add(measure.children, result.children); } }; add(measure.children, children); - results.push({ label: measure.label, timeElapsed, children }); + const cpuElapsed = measure.cpu.end - measure.cpu.start; + results.push({ + label: measure.label, + gpuElapsed: timeElapsed, + gpuAvg: gpuAvgs.add(measure.label, timeElapsed), + cpuElapsed, + cpuAvg: cpuAvgs.add(measure.label, cpuElapsed), + children, + calls: measure.calls, + }); } } else { unresolved.push(measure); @@ -126,7 +200,7 @@ export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions) return results; }, - mark: (label: string) => { + mark: (label: string, captureCalls = false) => { if (!dtq) return; if (pending.has(label)) { @@ -136,7 +210,14 @@ export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions) if (current !== null) { dtq.endQuery(dtq.TIME_ELAPSED); } - const measure: Measure = { label, queries: [], children: [], root: current === null }; + const measure: Measure = { + label, + queries: [], + children: [], + root: current === null, + cpu: { start: now(), end: -1 }, + captureCalls, + }; pending.set(label, measure); if (stack.length) { @@ -144,6 +225,14 @@ export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions) } stack.push(measure); + if (captureCalls) { + if (capturingCalls) { + throw new Error('Already capturing calls'); + } + clearStatsCalls(stats); + capturingCalls = true; + } + add(); }, markEnd: (label: string) => { @@ -160,6 +249,16 @@ export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions) dtq.endQuery(dtq.TIME_ELAPSED); pending.delete(label); + + measure.cpu.end = now(); + if (measure.captureCalls) { + measure.calls = { + drawInstanced: stats.calls.drawInstanced, + counts: stats.calls.counts, + }; + capturingCalls = false; + } + measures.push(measure); if (pending.size > 0) { @@ -168,6 +267,23 @@ export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions) current = null; } }, + stats: () => { + return { + gpu: gpuAvgs.stats(), + cpu: cpuAvgs.stats(), + }; + }, + formatedStats: () => { + const stats: Record<string, string> = {}; + const gpu = gpuAvgs.stats(); + const cpu = cpuAvgs.stats(); + for (const l of Object.keys(gpu)) { + const g = `${(gpu[l] / 1000 / 1000).toFixed(2)}`; + const c = `${cpu[l].toFixed(2)}`; + stats[l] = `${g} ms | CPU: ${c} ms`; + } + return stats; + }, clear, destroy: () => { clear(); @@ -176,15 +292,19 @@ export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions) } function formatTimerResult(result: TimerResult) { - const timeElapsed = result.timeElapsed / 1000 / 1000; - return `${result.label} ${timeElapsed.toFixed(2)}ms`; + const gpu = `${(result.gpuElapsed / 1000 / 1000).toFixed(2)}`; + const gpuAvg = `${(result.gpuAvg / 1000 / 1000).toFixed(2)}`; + const cpu = `${result.cpuElapsed.toFixed(2)}`; + const cpuAvg = `${result.cpuAvg.toFixed(2)}`; + return `${result.label} ${gpu} ms (avg. ${gpuAvg} ms) | CPU: ${cpu} ms (avg. ${cpuAvg} ms)`; } export function printTimerResults(results: TimerResult[]) { - return results.map(r => { + results.map(r => { const f = formatTimerResult(r); - if (r.children.length) { + if (r.children.length || r.calls) { console.groupCollapsed(f); + if (r.calls) console.log(r.calls); printTimerResults(r.children); console.groupEnd(); } else { diff --git a/src/mol-util/debug.ts b/src/mol-util/debug.ts index eb688aec3..c9d2cbffd 100644 --- a/src/mol-util/debug.ts +++ b/src/mol-util/debug.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -48,3 +48,23 @@ export function setDebugMode(value?: boolean) { export function setTimingMode(value?: boolean) { if (typeof value !== 'undefined') isTimingMode = value; } + +// + +type ConsoleStatsProvider = () => void +const consoleStatsProviders: ConsoleStatsProvider[] = []; + +export function addConsoleStatsProvider(p: ConsoleStatsProvider) { + if (!consoleStatsProviders.includes(p)) consoleStatsProviders.push(p); +} + +export function removeConsoleStatsProvider(p: ConsoleStatsProvider) { + const idx = consoleStatsProviders.indexOf(p); + if (idx !== -1) consoleStatsProviders.splice(idx, 1); +} + +export function consoleStats() { + for (const p of consoleStatsProviders) { + p(); + } +} -- GitLab