Browse Source

Merge pull request #446 from molstar/webgl-timer

Webgl timing support
Alexander Rose 2 years ago
parent
commit
d1e17785b8

+ 4 - 0
CHANGELOG.md

@@ -6,6 +6,10 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
+- GPU timing support
+    - Add ``timing-mode`` Viewer GET param
+    - Add support for webgl timer queries
+    - Add timer marks around GPU render & compute operations
 - Volume Server CIF: Add check that a data block contains volume data before parsing
 - Fix ``Scene.clear`` not clearing primitives & volumes arrays (@JonStargaryen)
 - Fix rendering volumes when wboit is switched off and postprocessing is enabled

+ 1 - 1
src/apps/viewer/app.ts

@@ -47,7 +47,7 @@ import '../../mol-util/polyfill';
 import { ObjectKeys } from '../../mol-util/type-helpers';
 
 export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
-export { setDebugMode, setProductionMode } from '../../mol-util/debug';
+export { setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
 
 const CustomFormats = [
     ['g3d', G3dProvider] as const

+ 4 - 1
src/apps/viewer/index.html

@@ -46,7 +46,10 @@
             }
 
             var debugMode = getParam('debug-mode', '[^&]+').trim() === '1';
-            if (debugMode) molstar.setDebugMode(debugMode, debugMode);
+            if (debugMode) molstar.setDebugMode(debugMode);
+
+            var timingMode = getParam('timing-mode', '[^&]+').trim() === '1';
+            if (timingMode) molstar.setTimingMode(timingMode);
 
             var hideControls = getParam('hide-controls', '[^&]+').trim() === '1';
             var collapseLeftPanel = getParam('collapse-left-panel', '[^&]+').trim() === '1';

+ 11 - 0
src/examples/alpha-orbitals/index.html

@@ -55,6 +55,17 @@
             </a>
         </div>
         <script>
+            function getParam(name, regex) {
+                var r = new RegExp(name + '=' + '(' + regex + ')[&]?', 'i');
+                return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
+            }
+
+            var debugMode = getParam('debug-mode', '[^&]+').trim() === '1';
+            if (debugMode) AlphaOrbitalsExample.setDebugMode(debugMode);
+
+            var timingMode = getParam('timing-mode', '[^&]+').trim() === '1';
+            if (timingMode) AlphaOrbitalsExample.setTimingMode(timingMode);
+
             AlphaOrbitalsExample.init('app')
         </script>
         <!-- __MOLSTAR_ANALYTICS__ -->

+ 6 - 2
src/examples/alpha-orbitals/index.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
@@ -25,6 +25,8 @@ import { DemoMoleculeSDF, DemoOrbitals } from './example-data';
 import './index.html';
 require('mol-plugin-ui/skin/light.scss');
 
+import { setDebugMode, setTimingMode } from '../../mol-util/debug';
+
 interface DemoInput {
     moleculeSdf: string,
     basis: Basis,
@@ -222,4 +224,6 @@ export class AlphaOrbitalsExample {
     }
 }
 
-(window as any).AlphaOrbitalsExample = new AlphaOrbitalsExample();
+(window as any).AlphaOrbitalsExample = new AlphaOrbitalsExample();
+(window as any).AlphaOrbitalsExample.setDebugMode = setDebugMode;
+(window as any).AlphaOrbitalsExample.setTimingMode = setTimingMode;

+ 5 - 4
src/extensions/alpha-orbitals/density.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
@@ -8,6 +8,7 @@ import { sortArray } from '../../mol-data/util';
 import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { Task } from '../../mol-task';
+import { isTimingMode } from '../../mol-util/debug';
 import { AlphaOrbital, createGrid, CubeGrid, CubeGridComputationParams, initCubeGrid } from './data-model';
 import { gpuComputeAlphaOrbitalsDensityGridValues } from './gpu/compute';
 
@@ -19,9 +20,9 @@ export function createSphericalCollocationDensityGrid(
 
         let matrix: Float32Array;
         if (canComputeGrid3dOnGPU(webgl)) {
-            // console.time('gpu');
-            matrix = await gpuComputeAlphaOrbitalsDensityGridValues(ctx, webgl!, cubeGrid, orbitals);
-            // console.timeEnd('gpu');
+            if (isTimingMode) webgl.timer.mark('createSphericalCollocationDensityGrid');
+            matrix = await gpuComputeAlphaOrbitalsDensityGridValues(ctx, webgl, cubeGrid, orbitals);
+            if (isTimingMode) webgl.timer.markEnd('createSphericalCollocationDensityGrid');
         } else {
             throw new Error('Missing OES_texture_float WebGL extension.');
         }

+ 5 - 6
src/extensions/alpha-orbitals/orbitals.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * Inspired by https://github.com/dgasmith/gau2grid.
  *
@@ -10,12 +10,11 @@ import { sortArray } from '../../mol-data/util';
 import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { Task } from '../../mol-task';
+import { isTimingMode } from '../../mol-util/debug';
 import { sphericalCollocation } from './collocation';
 import { AlphaOrbital, createGrid, CubeGrid, CubeGridComputationParams, initCubeGrid } from './data-model';
 import { gpuComputeAlphaOrbitalsGridValues } from './gpu/compute';
 
-// setDebugMode(true);
-
 export function createSphericalCollocationGrid(
     params: CubeGridComputationParams, orbital: AlphaOrbital, webgl?: WebGLContext
 ): Task<CubeGrid> {
@@ -24,9 +23,9 @@ export function createSphericalCollocationGrid(
 
         let matrix: Float32Array;
         if (canComputeGrid3dOnGPU(webgl)) {
-            // console.time('gpu');
-            matrix = await gpuComputeAlphaOrbitalsGridValues(ctx, webgl!, cubeGrid, orbital);
-            // console.timeEnd('gpu');
+            if (isTimingMode) webgl.timer.mark('createSphericalCollocationGrid');
+            matrix = await gpuComputeAlphaOrbitalsGridValues(ctx, webgl, cubeGrid, orbital);
+            if (isTimingMode) webgl.timer.markEnd('createSphericalCollocationGrid');
         } else {
             // console.time('cpu');
             matrix = await sphericalCollocation(cubeGrid, orbital, ctx);

+ 4 - 1
src/mol-canvas3d/canvas3d.ts

@@ -30,7 +30,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 } from '../mol-util/debug';
+import { isDebugMode, isTimingMode } from '../mol-util/debug';
 import { CameraHelperParams } from './helper/camera-helper';
 import { produce } from 'immer';
 import { HandleHelperParams } from './helper/handle-helper';
@@ -413,6 +413,7 @@ namespace Canvas3D {
                     cam = stereoCamera;
                 }
 
+                if (isTimingMode) webgl.timer.mark('Canvas3D.render');
                 const ctx = { renderer, camera: cam, scene, helper };
                 if (MultiSamplePass.isEnabled(p.multiSample)) {
                     const forceOn = !cameraChanged && markingUpdated && !controls.isAnimating;
@@ -420,6 +421,8 @@ namespace Canvas3D {
                 } else {
                     passes.draw.render(ctx, p, true);
                 }
+                if (isTimingMode) webgl.timer.markEnd('Canvas3D.render');
+
                 // if only marking has updated, do not set the flag to dirty
                 pickHelper.dirty = pickHelper.dirty || shouldRender;
                 didRender = true;

+ 3 - 0
src/mol-canvas3d/passes/draw.ts

@@ -20,6 +20,7 @@ import { WboitPass } from './wboit';
 import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './postprocessing';
 import { MarkingPass, MarkingProps } from './marking';
 import { CopyRenderable, createCopyRenderable } from '../../mol-gl/compute/util';
+import { isTimingMode } from '../../mol-util/debug';
 
 type Props = {
     postprocessing: PostprocessingProps
@@ -309,6 +310,7 @@ export class DrawPass {
     }
 
     render(ctx: RenderContext, props: Props, toDrawingBuffer: boolean) {
+        if (isTimingMode) this.webgl.timer.mark('DrawPass.render');
         const { renderer, camera, scene, helper } = ctx;
         renderer.setTransparentBackground(props.transparentBackground);
         renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());
@@ -320,6 +322,7 @@ export class DrawPass {
         } else {
             this._render(renderer, camera, scene, helper, toDrawingBuffer, props);
         }
+        if (isTimingMode) this.webgl.timer.markEnd('DrawPass.render');
     }
 
     getColorTarget(postprocessingProps: PostprocessingProps): RenderTarget {

+ 3 - 0
src/mol-canvas3d/passes/fxaa.ts

@@ -18,6 +18,7 @@ import { quad_vert } from '../../mol-gl/shader/quad.vert';
 import { fxaa_frag } from '../../mol-gl/shader/fxaa.frag';
 import { Viewport } from '../camera/util';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
+import { isTimingMode } from '../../mol-util/debug';
 
 export const FxaaParams = {
     edgeThresholdMin: PD.Numeric(0.0312, { min: 0.0312, max: 0.0833, step: 0.0001 }, { description: 'Trims the algorithm from processing darks.' }),
@@ -83,6 +84,7 @@ export class FxaaPass {
     }
 
     render(viewport: Viewport, target: RenderTarget | undefined) {
+        if (isTimingMode) this.webgl.timer.mark('FxaaPass.render');
         if (target) {
             target.bind();
         } else {
@@ -90,6 +92,7 @@ export class FxaaPass {
         }
         this.updateState(viewport);
         this.renderable.render();
+        if (isTimingMode) this.webgl.timer.markEnd('FxaaPass.render');
     }
 }
 

+ 3 - 0
src/mol-canvas3d/passes/marking.ts

@@ -20,6 +20,7 @@ import { Viewport } from '../camera/util';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { Color } from '../../mol-util/color';
 import { edge_frag } from '../../mol-gl/shader/marking/edge.frag';
+import { isTimingMode } from '../../mol-util/debug';
 
 export const MarkingParams = {
     enabled: PD.Boolean(true),
@@ -117,6 +118,7 @@ export class MarkingPass {
     }
 
     render(viewport: Viewport, target: RenderTarget | undefined) {
+        if (isTimingMode) this.webgl.timer.mark('MarkingPass.render');
         this.edgesTarget.bind();
         this.setEdgeState(viewport);
         this.edge.render();
@@ -128,6 +130,7 @@ export class MarkingPass {
         }
         this.setOverlayState(viewport);
         this.overlay.render();
+        if (isTimingMode) this.webgl.timer.markEnd('MarkingPass.render');
     }
 }
 

+ 5 - 0
src/mol-canvas3d/passes/multi-sample.ts

@@ -25,6 +25,7 @@ import { StereoCamera } from '../camera/stereo';
 import { quad_vert } from '../../mol-gl/shader/quad.vert';
 import { compose_frag } from '../../mol-gl/shader/compose.frag';
 import { MarkingProps } from './marking';
+import { isTimingMode } from '../../mol-util/debug';
 
 const ComposeSchema = {
     ...QuadSchema,
@@ -126,6 +127,7 @@ export class MultiSamplePass {
         const { camera } = ctx;
         const { compose, composeTarget, drawPass, webgl } = this;
         const { gl, state } = webgl;
+        if (isTimingMode) webgl.timer.mark('MultiSamplePass.renderMultiSample');
 
         // based on the Multisample Anti-Aliasing Render Pass
         // contributed to three.js by bhouston / http://clara.io/
@@ -198,12 +200,14 @@ export class MultiSamplePass {
 
         camera.viewOffset.enabled = false;
         camera.update();
+        if (isTimingMode) webgl.timer.markEnd('MultiSamplePass.renderMultiSample');
     }
 
     private renderTemporalMultiSample(sampleIndex: number, ctx: RenderContext, props: Props, toDrawingBuffer: boolean) {
         const { camera } = ctx;
         const { compose, composeTarget, holdTarget, drawPass, webgl } = this;
         const { gl, state } = webgl;
+        if (isTimingMode) webgl.timer.mark('MultiSamplePass.renderTemporalMultiSample');
 
         // based on the Multisample Anti-Aliasing Render Pass
         // contributed to three.js by bhouston / http://clara.io/
@@ -301,6 +305,7 @@ export class MultiSamplePass {
 
         camera.viewOffset.enabled = false;
         camera.update();
+        if (isTimingMode) webgl.timer.markEnd('MultiSamplePass.renderTemporalMultiSample');
 
         return sampleIndex >= offsetList.length ? -2 : sampleIndex;
     }

+ 7 - 0
src/mol-canvas3d/passes/pick.ts

@@ -11,6 +11,7 @@ import { WebGLContext } from '../../mol-gl/webgl/context';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { Vec3 } from '../../mol-math/linear-algebra';
 import { spiral2d } from '../../mol-math/misc';
+import { isTimingMode } from '../../mol-util/debug';
 import { unpackRGBToInt, unpackRGBAToDepth } from '../../mol-util/number-packing';
 import { Camera, ICamera } from '../camera';
 import { StereoCamera } from '../camera/stereo';
@@ -144,6 +145,7 @@ export class PickHelper {
     }
 
     private syncBuffers() {
+        if (isTimingMode) this.webgl.timer.mark('PickHelper.syncBuffers');
         const { pickX, pickY, pickWidth, pickHeight } = this;
 
         this.pickPass.objectPickTarget.bind();
@@ -157,6 +159,7 @@ export class PickHelper {
 
         this.pickPass.depthPickTarget.bind();
         this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.depthBuffer);
+        if (isTimingMode) this.webgl.timer.markEnd('PickHelper.syncBuffers');
     }
 
     private getBufferIdx(x: number, y: number): number {
@@ -175,6 +178,7 @@ export class PickHelper {
     }
 
     private render(camera: Camera | StereoCamera) {
+        if (isTimingMode) this.webgl.timer.mark('PickHelper.render');
         const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
         const { renderer, scene, helper } = this;
 
@@ -194,6 +198,7 @@ export class PickHelper {
         }
 
         this.dirty = false;
+        if (isTimingMode) this.webgl.timer.markEnd('PickHelper.render');
     }
 
     private identifyInternal(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
@@ -214,8 +219,10 @@ export class PickHelper {
         ) return;
 
         if (this.dirty) {
+            if (isTimingMode) this.webgl.timer.mark('PickHelper.identify');
             this.render(camera);
             this.syncBuffers();
+            if (isTimingMode) this.webgl.timer.markEnd('PickHelper.identify');
         }
 
         const xv = x - viewport.x;

+ 3 - 0
src/mol-canvas3d/passes/postprocessing.ts

@@ -27,6 +27,7 @@ import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
 import { Color } from '../../mol-util/color';
 import { FxaaParams, FxaaPass } from './fxaa';
 import { SmaaParams, SmaaPass } from './smaa';
+import { isTimingMode } from '../../mol-util/debug';
 
 const OutlinesSchema = {
     ...QuadSchema,
@@ -549,6 +550,7 @@ export class PostprocessingPass {
     }
 
     render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
+        if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
         this.updateState(camera, transparentBackground, backgroundColor, props);
 
         if (props.outline.name === 'on') {
@@ -585,6 +587,7 @@ export class PostprocessingPass {
         gl.clear(gl.COLOR_BUFFER_BIT);
 
         this.renderable.render();
+        if (isTimingMode) this.webgl.timer.markEnd('PostprocessingPass.render');
     }
 }
 

+ 3 - 2
src/mol-canvas3d/passes/smaa.ts

@@ -22,7 +22,7 @@ import { weights_frag } from '../../mol-gl/shader/smaa/weights.frag';
 import { edges_vert } from '../../mol-gl/shader/smaa/edges.vert';
 import { edges_frag } from '../../mol-gl/shader/smaa/edges.frag';
 import { Viewport } from '../camera/util';
-import { isDebugMode } from '../../mol-util/debug';
+import { isDebugMode, isTimingMode } from '../../mol-util/debug';
 
 export const SmaaParams = {
     edgeThreshold: PD.Numeric(0.1, { min: 0.05, max: 0.15, step: 0.01 }),
@@ -120,6 +120,7 @@ export class SmaaPass {
     }
 
     render(viewport: Viewport, target: RenderTarget | undefined) {
+        if (isTimingMode) this.webgl.timer.mark('SmaaPass.render');
         this.edgesTarget.bind();
         this.updateState(viewport);
         this.edgesRenderable.render();
@@ -135,8 +136,8 @@ export class SmaaPass {
         }
         this.updateState(viewport);
         this.blendRenderable.render();
+        if (isTimingMode) this.webgl.timer.markEnd('SmaaPass.render');
     }
-
 }
 
 //

+ 3 - 1
src/mol-canvas3d/passes/wboit.ts

@@ -17,7 +17,7 @@ import { quad_vert } from '../../mol-gl/shader/quad.vert';
 import { evaluateWboit_frag } from '../../mol-gl/shader/evaluate-wboit.frag';
 import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
 import { Vec2 } from '../../mol-math/linear-algebra';
-import { isDebugMode } from '../../mol-util/debug';
+import { isDebugMode, isTimingMode } from '../../mol-util/debug';
 
 const EvaluateWboitSchema = {
     ...QuadSchema,
@@ -71,6 +71,7 @@ export class WboitPass {
     }
 
     render() {
+        if (isTimingMode) this.webgl.timer.mark('WboitPass.render');
         const { state, gl } = this.webgl;
 
         state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
@@ -78,6 +79,7 @@ export class WboitPass {
 
         this.renderable.update();
         this.renderable.render();
+        if (isTimingMode) this.webgl.timer.markEnd('WboitPass.render');
     }
 
     setSize(width: number, height: number) {

+ 7 - 0
src/mol-geo/geometry/texture-mesh/color-smoothing.ts

@@ -20,6 +20,7 @@ import { accumulate_frag } from '../../../mol-gl/shader/compute/color-smoothing/
 import { accumulate_vert } from '../../../mol-gl/shader/compute/color-smoothing/accumulate.vert';
 import { isWebGL2 } from '../../../mol-gl/webgl/compat';
 import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh';
+import { isTimingMode } from '../../../mol-util/debug';
 
 export const ColorAccumulateSchema = {
     drawCount: ValueSpec('number'),
@@ -255,6 +256,7 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
     const { drawBuffers } = webgl.extensions;
     if (!drawBuffers) throw new Error('need WebGL draw buffers');
 
+    if (isTimingMode) webgl.timer.mark('calcTextureMeshColorSmoothing');
     const { gl, resources, state, extensions: { colorBufferHalfFloat, textureHalfFloat } } = webgl;
 
     const isInstanceType = input.colorType.endsWith('Instance');
@@ -321,6 +323,7 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
 
     const { uCurrentSlice, uCurrentX, uCurrentY } = accumulateRenderable.values;
 
+    if (isTimingMode) webgl.timer.mark('ColorAccumulate.render');
     setAccumulateDefaults(webgl);
     gl.viewport(0, 0, width, height);
     gl.scissor(0, 0, width, height);
@@ -349,6 +352,7 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
     accumulateTexture.detachFramebuffer(framebuffer, 0);
     countTexture.detachFramebuffer(framebuffer, 1);
     drawBuffers.drawBuffers([gl.COLOR_ATTACHMENT0, gl.NONE]);
+    if (isTimingMode) webgl.timer.markEnd('ColorAccumulate.render');
 
     // const accImage = new Float32Array(width * height * 4);
     // accumulateTexture.attachFramebuffer(framebuffer, 0);
@@ -364,6 +368,7 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
 
     // normalize
 
+    if (isTimingMode) webgl.timer.mark('ColorNormalize.render');
     if (!texture) texture = resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
     texture.define(width, height);
 
@@ -376,6 +381,7 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
     gl.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     normalizeRenderable.render();
+    if (isTimingMode) webgl.timer.markEnd('ColorNormalize.render');
 
     // const normImage = new Uint8Array(width * height * 4);
     // texture.attachFramebuffer(framebuffer, 0);
@@ -385,6 +391,7 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
 
     const gridTransform = Vec4.create(min[0], min[1], min[2], scaleFactor);
     const type = isInstanceType ? 'volumeInstance' : 'volume';
+    if (isTimingMode) webgl.timer.markEnd('calcTextureMeshColorSmoothing');
 
     return { texture, gridDim, gridTexDim: Vec2.create(width, height), gridTransform, type };
 }

+ 18 - 7
src/mol-gl/compute/grid3d.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
@@ -18,8 +18,9 @@ import { createComputeRenderItem } from '../webgl/render-item';
 import { createComputeRenderable } from '../renderable';
 import { isLittleEndian } from '../../mol-util/is-little-endian';
 import { RuntimeContext } from '../../mol-task';
+import { isTimingMode } from '../../mol-util/debug';
 
-export function canComputeGrid3dOnGPU(webgl?: WebGLContext) {
+export function canComputeGrid3dOnGPU(webgl?: WebGLContext): webgl is WebGLContext {
     return !!webgl?.extensions.textureFloat;
 }
 
@@ -159,7 +160,8 @@ export function createGrid3dComputeRenderable<S extends RenderableSchema, P, CS>
 
         const array = new Uint8Array(uWidth * uWidth * 4);
         if (spec.cumulative) {
-            const { gl } = webgl;
+            const { gl, state } = webgl;
+            if (isTimingMode) webgl.timer.mark('Grid3dCompute.renderCumulative');
 
             const states = spec.cumulative.states(params);
 
@@ -167,7 +169,7 @@ export function createGrid3dComputeRenderable<S extends RenderableSchema, P, CS>
             tex[1].define(uWidth, uWidth);
 
             resetGl(webgl, uWidth);
-            gl.clearColor(0, 0, 0, 0);
+            state.clearColor(0, 0, 0, 0);
 
             tex[0].attachFramebuffer(framebuffer, 'color0');
             gl.clear(gl.COLOR_BUFFER_BIT);
@@ -175,12 +177,13 @@ export function createGrid3dComputeRenderable<S extends RenderableSchema, P, CS>
             tex[1].attachFramebuffer(framebuffer, 'color0');
             gl.clear(gl.COLOR_BUFFER_BIT);
 
-            if (spec.cumulative.yieldPeriod) {
+            if (spec.cumulative.yieldPeriod && !isTimingMode) {
                 await ctx.update({ message: 'Computing...', isIndeterminate: false, current: 0, max: states.length });
             }
 
             const yieldPeriod = Math.max(1, spec.cumulative.yieldPeriod ?? 1 | 0);
 
+            if (isTimingMode) webgl.timer.mark('Grid3dCompute.renderBatch');
             for (let i = 0; i < states.length; i++) {
                 ValueCell.update(cells.tCumulativeSum, tex[(i + 1) % 2]);
                 tex[i % 2].attachFramebuffer(framebuffer, 'color0');
@@ -191,23 +194,31 @@ export function createGrid3dComputeRenderable<S extends RenderableSchema, P, CS>
 
                 if (spec.cumulative.yieldPeriod && i !== states.length - 1) {
                     if (i % yieldPeriod === yieldPeriod - 1) {
-                        webgl.readPixels(0, 0, 1, 1, array);
+                        webgl.waitForGpuCommandsCompleteSync();
+                        if (isTimingMode) webgl.timer.markEnd('Grid3dCompute.renderBatch');
+                        if (isTimingMode) webgl.timer.mark('Grid3dCompute.renderBatch');
                     }
-                    if (ctx.shouldUpdate) {
+                    if (ctx.shouldUpdate && !isTimingMode) {
                         await ctx.update({ current: i + 1 });
                     }
                 }
             }
+            if (isTimingMode) webgl.timer.markEnd('Grid3dCompute.renderBatch');
+            if (isTimingMode) webgl.timer.markEnd('Grid3dCompute.renderCumulative');
         } else {
+            if (isTimingMode) webgl.timer.mark('Grid3dCompute.render');
             tex[0].define(uWidth, uWidth);
             tex[0].attachFramebuffer(framebuffer, 'color0');
             framebuffer.bind();
             resetGl(webgl, uWidth);
             renderable.update();
             renderable.render();
+            if (isTimingMode) webgl.timer.markEnd('Grid3dCompute.render');
         }
 
+        if (isTimingMode) webgl.timer.mark('Grid3dCompute.readPixels');
         webgl.readPixels(0, 0, uWidth, uWidth, array);
+        if (isTimingMode) webgl.timer.markEnd('Grid3dCompute.readPixels');
         return new Float32Array(array.buffer, array.byteOffset, nx * ny * nz);
     };
 }

+ 4 - 1
src/mol-gl/compute/histogram-pyramid/reduction.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -19,6 +19,7 @@ import { isPowerOfTwo } from '../../../mol-math/misc';
 import { quad_vert } from '../../../mol-gl/shader/quad.vert';
 import { reduction_frag } from '../../../mol-gl/shader/histogram-pyramid/reduction.frag';
 import { isWebGL2 } from '../../webgl/compat';
+import { isTimingMode } from '../../../mol-util/debug';
 
 const HistopyramidReductionSchema = {
     ...QuadSchema,
@@ -120,6 +121,7 @@ export interface HistogramPyramid {
 }
 
 export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, scale: Vec2, gridTexDim: Vec3): HistogramPyramid {
+    if (isTimingMode) ctx.timer.mark('createHistogramPyramid');
     const { gl } = ctx;
     const w = inputTexture.getWidth();
     const h = inputTexture.getHeight();
@@ -193,6 +195,7 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
     }
 
     gl.finish();
+    if (isTimingMode) ctx.timer.markEnd('createHistogramPyramid');
 
     // printTexture(ctx, pyramidTex, 2)
 

+ 4 - 1
src/mol-gl/compute/histogram-pyramid/sum.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -16,6 +16,7 @@ import { QuadSchema, QuadValues } from '../util';
 import { quad_vert } from '../../../mol-gl/shader/quad.vert';
 import { sum_frag } from '../../../mol-gl/shader/histogram-pyramid/sum.frag';
 import { isWebGL2 } from '../../webgl/compat';
+import { isTimingMode } from '../../../mol-util/debug';
 
 const HistopyramidSumSchema = {
     ...QuadSchema,
@@ -66,6 +67,7 @@ const sumBytes = new Uint8Array(4);
 const sumInts = new Int32Array(4);
 
 export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture) {
+    if (isTimingMode) ctx.timer.mark('getHistopyramidSum');
     const { gl, resources } = ctx;
 
     const renderable = getHistopyramidSumRenderable(ctx, pyramidTopTexture);
@@ -93,6 +95,7 @@ export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture
 
     ctx.readPixels(0, 0, 1, 1, isWebGL2(gl) ? sumInts : sumBytes);
     ctx.unbindFramebuffer();
+    if (isTimingMode) ctx.timer.markEnd('getHistopyramidSum');
 
     return isWebGL2(gl)
         ? sumInts[0]

+ 4 - 1
src/mol-gl/compute/marching-cubes/active-voxels.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -16,6 +16,7 @@ import { QuadSchema, QuadValues } from '../util';
 import { getTriCount } from './tables';
 import { quad_vert } from '../../../mol-gl/shader/quad.vert';
 import { activeVoxels_frag } from '../../../mol-gl/shader/marching-cubes/active-voxels.frag';
+import { isTimingMode } from '../../../mol-util/debug';
 
 const ActiveVoxelsSchema = {
     ...QuadSchema,
@@ -83,6 +84,7 @@ function setRenderingDefaults(ctx: WebGLContext) {
 }
 
 export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, gridScale: Vec2) {
+    if (isTimingMode) ctx.timer.mark('calcActiveVoxels');
     const { gl, resources } = ctx;
     const width = volumeData.getWidth();
     const height = volumeData.getHeight();
@@ -115,6 +117,7 @@ export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim
     // console.log('at', readTexture(ctx, activeVoxelsTex));
 
     gl.finish();
+    if (isTimingMode) ctx.timer.markEnd('calcActiveVoxels');
 
     return activeVoxelsTex;
 }

+ 6 - 12
src/mol-gl/compute/marching-cubes/isosurface.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -19,6 +19,7 @@ import { quad_vert } from '../../../mol-gl/shader/quad.vert';
 import { isosurface_frag } from '../../../mol-gl/shader/marching-cubes/isosurface.frag';
 import { calcActiveVoxels } from './active-voxels';
 import { isWebGL2 } from '../../webgl/compat';
+import { isTimingMode } from '../../../mol-util/debug';
 
 const IsosurfaceSchema = {
     ...QuadSchema,
@@ -122,6 +123,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
     const { drawBuffers } = ctx.extensions;
     if (!drawBuffers) throw new Error('need WebGL draw buffers');
 
+    if (isTimingMode) ctx.timer.mark('createIsosurfaceBuffers');
     const { gl, resources, extensions } = ctx;
     const { pyramidTex, height, levels, scale, count } = histogramPyramid;
     const width = pyramidTex.getWidth();
@@ -192,6 +194,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
     renderable.render();
 
     gl.finish();
+    if (isTimingMode) ctx.timer.markEnd('createIsosurfaceBuffers');
 
     return { vertexTexture, groupTexture, normalTexture, vertexCount: count };
 }
@@ -208,20 +211,11 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
  * Implementation based on http://www.miaumiau.cat/2016/10/stream-compaction-in-webgl/
  */
 export function extractIsosurface(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, gridTexScale: Vec2, transform: Mat4, isoValue: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, vertexTexture?: Texture, groupTexture?: Texture, normalTexture?: Texture) {
-    // console.time('calcActiveVoxels');
+    if (isTimingMode) ctx.timer.mark('extractIsosurface');
     const activeVoxelsTex = calcActiveVoxels(ctx, volumeData, gridDim, gridTexDim, isoValue, gridTexScale);
-    // ctx.waitForGpuCommandsCompleteSync();
-    // console.timeEnd('calcActiveVoxels');
-
-    // console.time('createHistogramPyramid');
     const compacted = createHistogramPyramid(ctx, activeVoxelsTex, gridTexScale, gridTexDim);
-    // ctx.waitForGpuCommandsCompleteSync();
-    // console.timeEnd('createHistogramPyramid');
-
-    // console.time('createIsosurfaceBuffers');
     const gv = createIsosurfaceBuffers(ctx, activeVoxelsTex, volumeData, compacted, gridDim, gridTexDim, transform, isoValue, invert, packedGroup, axisOrder, vertexTexture, groupTexture, normalTexture);
-    // ctx.waitForGpuCommandsCompleteSync();
-    // console.timeEnd('createIsosurfaceBuffers');
+    if (isTimingMode) ctx.timer.markEnd('extractIsosurface');
 
     return gv;
 }

+ 23 - 0
src/mol-gl/renderer.ts

@@ -19,6 +19,7 @@ import { degToRad } from '../mol-math/misc';
 import { Texture, Textures } from './webgl/texture';
 import { arrayMapUpsert } from '../mol-util/array';
 import { clamp } from '../mol-math/interpolate';
+import { isTimingMode } from '../mol-util/debug';
 
 export interface RendererStats {
     programCount: number
@@ -360,6 +361,7 @@ namespace Renderer {
         };
 
         const renderPick = (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, depthTexture: Texture | null, pickType: PickType) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderPick');
             state.disable(gl.BLEND);
             state.enable(gl.DEPTH_TEST);
             state.depthMask(true);
@@ -373,9 +375,11 @@ namespace Renderer {
                     renderObject(renderables[i], variant, Flag.None);
                 }
             }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderPick');
         };
 
         const renderDepth = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderDepth');
             state.disable(gl.BLEND);
             state.enable(gl.DEPTH_TEST);
             state.depthMask(true);
@@ -386,9 +390,11 @@ namespace Renderer {
             for (let i = 0, il = renderables.length; i < il; ++i) {
                 renderObject(renderables[i], 'depth', Flag.None);
             }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderDepth');
         };
 
         const renderDepthOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderDepthOpaque');
             state.disable(gl.BLEND);
             state.enable(gl.DEPTH_TEST);
             state.depthMask(true);
@@ -402,9 +408,11 @@ namespace Renderer {
                     renderObject(r, 'depth', Flag.None);
                 }
             }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderDepthOpaque');
         };
 
         const renderDepthTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderDepthTransparent');
             state.disable(gl.BLEND);
             state.enable(gl.DEPTH_TEST);
             state.depthMask(true);
@@ -418,9 +426,11 @@ namespace Renderer {
                     renderObject(r, 'depth', Flag.None);
                 }
             }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderDepthTransparent');
         };
 
         const renderMarkingDepth = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderMarkingDepth');
             state.disable(gl.BLEND);
             state.enable(gl.DEPTH_TEST);
             state.depthMask(true);
@@ -436,9 +446,11 @@ namespace Renderer {
                     renderObject(renderables[i], 'marking', Flag.None);
                 }
             }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderMarkingDepth');
         };
 
         const renderMarkingMask = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderMarkingMask');
             state.disable(gl.BLEND);
             state.enable(gl.DEPTH_TEST);
             state.depthMask(true);
@@ -454,6 +466,7 @@ namespace Renderer {
                     renderObject(renderables[i], 'marking', Flag.None);
                 }
             }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderMarkingMask');
         };
 
         const renderBlended = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
@@ -462,6 +475,7 @@ namespace Renderer {
         };
 
         const renderBlendedOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderBlendedOpaque');
             state.disable(gl.BLEND);
             state.enable(gl.DEPTH_TEST);
             state.depthMask(true);
@@ -477,9 +491,11 @@ namespace Renderer {
                     renderObject(r, 'colorBlended', Flag.BlendedBack);
                 }
             }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderBlendedOpaque');
         };
 
         const renderBlendedTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderBlendedTransparent');
             state.enable(gl.DEPTH_TEST);
 
             updateInternal(group, camera, depthTexture, Mask.Transparent, false);
@@ -516,9 +532,11 @@ namespace Renderer {
                     }
                 }
             }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderBlendedTransparent');
         };
 
         const renderBlendedVolume = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderBlendedVolume');
             state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
             state.enable(gl.BLEND);
 
@@ -531,9 +549,11 @@ namespace Renderer {
                     renderObject(r, 'colorBlended', Flag.None);
                 }
             }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderBlendedVolume');
         };
 
         const renderWboitOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderWboitOpaque');
             state.disable(gl.BLEND);
             state.enable(gl.DEPTH_TEST);
             state.depthMask(true);
@@ -551,9 +571,11 @@ namespace Renderer {
                     renderObject(r, 'colorWboit', Flag.None);
                 }
             }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderWboitOpaque');
         };
 
         const renderWboitTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderWboitTransparent');
             updateInternal(group, camera, depthTexture, Mask.Transparent, false);
 
             const { renderables } = group;
@@ -567,6 +589,7 @@ namespace Renderer {
                     renderObject(r, 'colorWboit', Flag.None);
                 }
             }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderWboitTransparent');
         };
 
         return {

+ 82 - 1
src/mol-gl/webgl/compat.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -320,6 +320,87 @@ export function getSRGB(gl: GLRenderingContext): COMPAT_sRGB | null {
     }
 }
 
+export interface COMPAT_disjoint_timer_query {
+    /** A GLint indicating the number of bits used to hold the query result for the given target. */
+    QUERY_COUNTER_BITS: number
+    /** A WebGLQuery object, which is the currently active query for the given target. */
+    CURRENT_QUERY: number
+    /** A GLuint64EXT containing the query result. */
+    QUERY_RESULT: number
+    /** A GLboolean indicating whether or not a query result is available. */
+    QUERY_RESULT_AVAILABLE: number
+    /** Elapsed time (in nanoseconds). */
+    TIME_ELAPSED: number
+    /** The current time. */
+    TIMESTAMP: number
+    /** A GLboolean indicating whether or not the GPU performed any disjoint operation. */
+    GPU_DISJOINT: number
+
+    /** Creates a new WebGLTimerQueryEXT. */
+    createQuery: () => WebGLQuery
+    /** Deletes a given WebGLTimerQueryEXT. */
+    deleteQuery: (query: WebGLQuery) => void
+    /** Returns true if a given object is a valid WebGLTimerQueryEXT. */
+    isQuery: (query: WebGLQuery) => boolean
+    /** The timer starts when all commands prior to beginQueryEXT have been fully executed. */
+    beginQuery: (target: number, query: WebGLQuery) => void
+    /** The timer stops when all commands prior to endQueryEXT have been fully executed. */
+    endQuery: (target: number) => void
+    /** Records the current time into the corresponding query object. */
+    queryCounter: (query: WebGLQuery, target: number) => void
+    /** Returns information about a query target. */
+    getQuery: (target: number, pname: number) => WebGLQuery | number
+    /** Return the state of a query object. */
+    getQueryParameter: (query: WebGLQuery, pname: number) => number | boolean
+}
+
+export function getDisjointTimerQuery(gl: GLRenderingContext): COMPAT_disjoint_timer_query | null {
+    if (isWebGL2(gl)) {
+        // Firefox has EXT_disjoint_timer_query in webgl2
+        const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2') || gl.getExtension('EXT_disjoint_timer_query');
+        if (ext === null) return null;
+        return {
+            QUERY_COUNTER_BITS: ext.QUERY_COUNTER_BITS_EXT,
+            CURRENT_QUERY: gl.CURRENT_QUERY,
+            QUERY_RESULT: gl.QUERY_RESULT,
+            QUERY_RESULT_AVAILABLE: gl.QUERY_RESULT_AVAILABLE,
+            TIME_ELAPSED: ext.TIME_ELAPSED_EXT,
+            TIMESTAMP: ext.TIMESTAMP_EXT,
+            GPU_DISJOINT: ext.GPU_DISJOINT_EXT,
+
+            createQuery: gl.createQuery.bind(gl),
+            deleteQuery: gl.deleteQuery.bind(gl),
+            isQuery: gl.isQuery.bind(gl),
+            beginQuery: gl.beginQuery.bind(gl),
+            endQuery: gl.endQuery.bind(gl),
+            queryCounter: ext.queryCounterEXT.bind(ext),
+            getQuery: gl.getQuery.bind(gl),
+            getQueryParameter: gl.getQueryParameter.bind(gl),
+        };
+    } else {
+        const ext = gl.getExtension('EXT_disjoint_timer_query');
+        if (ext === null) return null;
+        return {
+            QUERY_COUNTER_BITS: ext.QUERY_COUNTER_BITS_EXT,
+            CURRENT_QUERY: ext.CURRENT_QUERY_EXT,
+            QUERY_RESULT: ext.QUERY_RESULT_EXT,
+            QUERY_RESULT_AVAILABLE: ext.QUERY_RESULT_AVAILABLE_EXT,
+            TIME_ELAPSED: ext.TIME_ELAPSED_EXT,
+            TIMESTAMP: ext.TIMESTAMP_EXT,
+            GPU_DISJOINT: ext.GPU_DISJOINT_EXT,
+
+            createQuery: ext.createQueryEXT.bind(ext),
+            deleteQuery: ext.deleteQueryEXT.bind(ext),
+            isQuery: ext.isQueryEXT.bind(ext),
+            beginQuery: ext.beginQueryEXT.bind(ext),
+            endQuery: ext.endQueryEXT.bind(ext),
+            queryCounter: ext.queryCounterEXT.bind(ext),
+            getQuery: ext.getQueryEXT.bind(ext),
+            getQueryParameter: ext.getQueryObjectEXT.bind(ext),
+        };
+    }
+}
+
 //
 
 const TextureTestVertShader = `

+ 4 - 0
src/mol-gl/webgl/context.ts

@@ -17,6 +17,7 @@ import { BehaviorSubject } from 'rxjs';
 import { now } from '../../mol-util/now';
 import { Texture, TextureFilter } from './texture';
 import { ComputeRenderable } from '../renderable';
+import { createTimer, WebGLTimer } from './timer';
 
 export function getGLContext(canvas: HTMLCanvasElement, attribs?: WebGLContextAttributes & { preferWebGl1?: boolean }): GLRenderingContext | null {
     function get(id: 'webgl' | 'experimental-webgl' | 'webgl2') {
@@ -186,6 +187,7 @@ export interface WebGLContext {
     readonly state: WebGLState
     readonly stats: WebGLStats
     readonly resources: WebGLResources
+    readonly timer: WebGLTimer
 
     readonly maxTextureSize: number
     readonly max3dTextureSize: number
@@ -221,6 +223,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 parameters = {
         maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
@@ -289,6 +292,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
         state,
         stats,
         resources,
+        timer,
 
         get maxTextureSize() { return parameters.maxTextureSize; },
         get max3dTextureSize() { return parameters.max3dTextureSize; },

+ 8 - 2
src/mol-gl/webgl/extensions.ts

@@ -1,10 +1,10 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod, getDepthTexture, COMPAT_depth_texture, COMPAT_sRGB, getSRGB, getTextureHalfFloat, getTextureHalfFloatLinear, COMPAT_texture_half_float, COMPAT_texture_half_float_linear, COMPAT_color_buffer_half_float, getColorBufferHalfFloat, getVertexArrayObject } from './compat';
+import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod, getDepthTexture, COMPAT_depth_texture, COMPAT_sRGB, getSRGB, getTextureHalfFloat, getTextureHalfFloatLinear, COMPAT_texture_half_float, COMPAT_texture_half_float_linear, COMPAT_color_buffer_half_float, getColorBufferHalfFloat, getVertexArrayObject, getDisjointTimerQuery, COMPAT_disjoint_timer_query } from './compat';
 import { isDebugMode } from '../../mol-util/debug';
 
 export type WebGLExtensions = {
@@ -25,6 +25,7 @@ export type WebGLExtensions = {
     drawBuffers: COMPAT_draw_buffers | null
     shaderTextureLod: COMPAT_shader_texture_lod | null
     sRGB: COMPAT_sRGB | null
+    disjointTimerQuery: COMPAT_disjoint_timer_query | null
 }
 
 export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
@@ -99,6 +100,10 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
     if (isDebugMode && sRGB === null) {
         console.log('Could not find support for "sRGB"');
     }
+    const disjointTimerQuery = getDisjointTimerQuery(gl);
+    if (isDebugMode && disjointTimerQuery === null) {
+        console.log('Could not find support for "disjoint_timer_query"');
+    }
 
     return {
         instancedArrays,
@@ -118,5 +123,6 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
         drawBuffers,
         shaderTextureLod,
         sRGB,
+        disjointTimerQuery,
     };
 }

+ 194 - 0
src/mol-gl/webgl/timer.ts

@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { GLRenderingContext } from './compat';
+import { WebGLExtensions } from './extensions';
+
+export type TimerResult = {
+    readonly label: string
+    readonly timeElapsed: number
+    readonly children: TimerResult[]
+}
+
+function getQuery(extensions: WebGLExtensions) {
+    return extensions.disjointTimerQuery ? extensions.disjointTimerQuery.createQuery() : null;
+}
+
+export type WebGLTimer = {
+    /** Check with GPU for finished timers. */
+    resolve: () => TimerResult[]
+    mark: (label: string) => void
+    markEnd: (label: string) => void
+    clear: () => void
+    destroy: () => void
+}
+
+type Measure = { label: string, queries: WebGLQuery[], children: Measure[], root: boolean, timeElapsed?: number };
+type QueryResult = { timeElapsed?: number, refCount: number };
+
+export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions): WebGLTimer {
+    const dtq = extensions.disjointTimerQuery;
+
+    const queries = new Map<WebGLQuery, QueryResult>();
+    const pending = new Map<string, Measure>();
+    const stack: Measure[] = [];
+
+    let measures: Measure[] = [];
+    let current: WebGLQuery | null = null;
+
+    const clear = () => {
+        if (!dtq) return;
+
+        queries.forEach((_, query) => {
+            dtq.deleteQuery(query);
+        });
+        pending.clear();
+        measures = [];
+        current = null;
+    };
+
+    const add = () => {
+        if (!dtq) return;
+
+        const query = getQuery(extensions);
+        if (!query) return;
+
+        dtq.beginQuery(dtq.TIME_ELAPSED, query);
+        pending.forEach((measure, _) => {
+            measure.queries.push(query);
+        });
+        queries.set(query, { refCount: pending.size });
+        current = query;
+    };
+
+    return {
+        resolve: () => {
+            const results: TimerResult[] = [];
+            if (!dtq || !measures.length) return results;
+            // console.log('resolve');
+            queries.forEach((result, query) => {
+                if (result.timeElapsed !== undefined) return;
+
+                const available = dtq.getQueryParameter(query, dtq.QUERY_RESULT_AVAILABLE);
+                const disjoint = gl.getParameter(dtq.GPU_DISJOINT);
+
+                if (available && !disjoint) {
+                    const timeElapsed = dtq.getQueryParameter(query, dtq.QUERY_RESULT) as number;
+                    result.timeElapsed = timeElapsed;
+                    // console.log('timeElapsed', result.timeElapsed);
+                }
+
+                if (available || disjoint) {
+                    dtq.deleteQuery(query);
+                }
+            });
+
+            const unresolved: Measure[] = [];
+            for (const measure of measures) {
+                if (measure.queries.every(q => queries.get(q)?.timeElapsed !== undefined)) {
+                    let timeElapsed = 0;
+                    for (const query of measure.queries) {
+                        const result = queries.get(query)!;
+                        timeElapsed += result.timeElapsed!;
+                        result.refCount -= 1;
+                    }
+                    measure.timeElapsed = timeElapsed;
+                    if (measure.root) {
+                        const children: TimerResult[] = [];
+                        const add = (measures: Measure[], children: TimerResult[]) => {
+                            for (const measure of measures) {
+                                const result: TimerResult = {
+                                    label: measure.label,
+                                    timeElapsed: measure.timeElapsed!,
+                                    children: []
+                                };
+                                children.push(result);
+                                add(measure.children, result.children);
+                            }
+                        };
+                        add(measure.children, children);
+                        results.push({ label: measure.label, timeElapsed, children });
+                    }
+                } else {
+                    unresolved.push(measure);
+                }
+            }
+            measures = unresolved;
+
+            queries.forEach((result, query) => {
+                if (result.refCount === 0) {
+                    queries.delete(query);
+                }
+            });
+
+            return results;
+        },
+        mark: (label: string) => {
+            if (!dtq) return;
+
+            if (pending.has(label)) {
+                throw new Error(`Timer mark for '${label}' already exists`);
+            }
+
+            if (current !== null) {
+                dtq.endQuery(dtq.TIME_ELAPSED);
+            }
+            const measure: Measure = { label, queries: [], children: [], root: current === null };
+            pending.set(label, measure);
+
+            if (stack.length) {
+                stack[stack.length - 1].children.push(measure);
+            }
+            stack.push(measure);
+
+            add();
+        },
+        markEnd: (label: string) => {
+            if (!dtq) return;
+
+            const measure = pending.get(label);
+            if (!measure) {
+                throw new Error(`Timer mark for '${label}' does not exist`);
+            }
+
+            if (stack.pop()?.label !== label) {
+                throw new Error(`Timer mark for '${label}' has pending nested mark`);
+            }
+
+            dtq.endQuery(dtq.TIME_ELAPSED);
+            pending.delete(label);
+            measures.push(measure);
+
+            if (pending.size > 0) {
+                add();
+            } else {
+                current = null;
+            }
+        },
+        clear,
+        destroy: () => {
+            clear();
+        }
+    };
+}
+
+function formatTimerResult(result: TimerResult) {
+    const timeElapsed = result.timeElapsed / 1000 / 1000;
+    return `${result.label} ${timeElapsed.toFixed(2)}ms`;
+}
+
+export function printTimerResults(results: TimerResult[]) {
+    return results.map(r => {
+        const f = formatTimerResult(r);
+        if (r.children.length) {
+            console.groupCollapsed(f);
+            printTimerResults(r.children);
+            console.groupEnd();
+        } else {
+            console.log(f);
+        }
+    });
+}

+ 9 - 2
src/mol-math/geometry/gaussian-density/gpu.ts

@@ -21,6 +21,7 @@ import { ValueSpec, AttributeSpec, UniformSpec, TextureSpec, DefineSpec, Values
 import { gaussianDensity_vert } from '../../../mol-gl/shader/gaussian-density.vert';
 import { gaussianDensity_frag } from '../../../mol-gl/shader/gaussian-density.frag';
 import { Framebuffer } from '../../../mol-gl/webgl/framebuffer';
+import { isTimingMode } from '../../../mol-util/debug';
 
 const GaussianDensitySchema = {
     drawCount: ValueSpec('number'),
@@ -85,11 +86,17 @@ export function GaussianDensityTexture(webgl: WebGLContext, position: PositionDa
 }
 
 export function GaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, powerOfTwo: boolean, props: GaussianDensityProps, oldTexture?: Texture): GaussianDensityTextureData {
-    return finalizeGaussianDensityTexture(calcGaussianDensityTexture2d(webgl, position, box, radius, powerOfTwo, props, oldTexture));
+    if (isTimingMode) webgl.timer.mark('GaussianDensityTexture2d');
+    const data = calcGaussianDensityTexture2d(webgl, position, box, radius, powerOfTwo, props, oldTexture);
+    if (isTimingMode) webgl.timer.markEnd('GaussianDensityTexture2d');
+    return finalizeGaussianDensityTexture(data);
 }
 
 export function GaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, oldTexture?: Texture): GaussianDensityTextureData {
-    return finalizeGaussianDensityTexture(calcGaussianDensityTexture3d(webgl, position, box, radius, props, oldTexture));
+    if (isTimingMode) webgl.timer.mark('GaussianDensityTexture3d');
+    const data = calcGaussianDensityTexture3d(webgl, position, box, radius, props, oldTexture);
+    if (isTimingMode) webgl.timer.markEnd('GaussianDensityTexture3d');
+    return finalizeGaussianDensityTexture(data);
 }
 
 function finalizeGaussianDensityTexture({ texture, scale, bbox, gridDim, gridTexDim, gridTexScale, radiusFactor, resolution, maxRadius }: _GaussianDensityTextureData): GaussianDensityTextureData {

+ 13 - 1
src/mol-plugin/animation-loop.ts

@@ -1,12 +1,15 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 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>
  */
 
 import { PluginContext } from './context';
 import { now } from '../mol-util/now';
 import { PluginAnimationManager } from '../mol-plugin-state/manager/animation';
+import { isTimingMode } from '../mol-util/debug';
+import { printTimerResults } from '../mol-gl/webgl/timer';
 
 export class PluginAnimationLoop {
     private currentFrame: any = void 0;
@@ -19,6 +22,15 @@ export class PluginAnimationLoop {
     async tick(t: number, options?: { isSynchronous?: boolean, manualDraw?: boolean, animation?: PluginAnimationManager.AnimationInfo }) {
         await this.plugin.managers.animation.tick(t, options?.isSynchronous, options?.animation);
         this.plugin.canvas3d?.tick(t as now.Timestamp, options);
+
+        if (isTimingMode) {
+            const timerResults = this.plugin.canvas3d?.webgl.timer.resolve();
+            if (timerResults) {
+                for (const result of timerResults) {
+                    printTimerResults([result]);
+                }
+            }
+        }
     }
 
     private frame = () => {

+ 5 - 12
src/mol-repr/structure/visual/gaussian-surface-mesh.ts

@@ -27,6 +27,7 @@ import { applyMeshColorSmoothing } from '../../../mol-geo/geometry/mesh/color-sm
 import { applyTextureMeshColorSmoothing } from '../../../mol-geo/geometry/texture-mesh/color-smoothing';
 import { ColorSmoothingParams, getColorSmoothingProps } from '../../../mol-geo/geometry/base';
 import { Vec3 } from '../../../mol-math/linear-algebra';
+import { isTimingMode } from '../../../mol-util/debug';
 
 const SharedParams = {
     ...GaussianDensityParams,
@@ -213,6 +214,7 @@ const GaussianSurfaceName = 'gaussian-surface';
 async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, textureMesh?: TextureMesh): Promise<TextureMesh> {
     if (!ctx.webgl) throw new Error('webgl context required to create gaussian surface texture-mesh');
 
+    if (isTimingMode) ctx.webgl.timer.mark('createGaussianSurfaceTextureMesh');
     const { namedTextures, resources, extensions: { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } } = ctx.webgl;
     if (!namedTextures[GaussianSurfaceName]) {
         namedTextures[GaussianSurfaceName] = colorBufferHalfFloat && textureHalfFloat
@@ -222,18 +224,13 @@ async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit,
                 : resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
     }
 
-    // console.time('computeUnitGaussianDensityTexture2d');
     const densityTextureData = await computeUnitGaussianDensityTexture2d(structure, unit, theme.size, true, props, ctx.webgl, namedTextures[GaussianSurfaceName]).runInContext(ctx.runtime);
-    // console.log(densityTextureData);
-    // console.log('vertexGroupTexture', readTexture(ctx.webgl, densityTextureData.texture));
-    // ctx.webgl.waitForGpuCommandsCompleteSync();
-    // console.timeEnd('computeUnitGaussianDensityTexture2d');
-
     const isoLevel = Math.exp(-props.smoothness) / densityTextureData.radiusFactor;
 
     const axisOrder = Vec3.create(0, 1, 2);
     const buffer = textureMesh?.doubleBuffer.get();
     const gv = extractIsosurface(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridTexScale, densityTextureData.transform, isoLevel, false, true, axisOrder, buffer?.vertex, buffer?.group, buffer?.normal);
+    if (isTimingMode) ctx.webgl.timer.markEnd('createGaussianSurfaceTextureMesh');
 
     const boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, densityTextureData.maxRadius);
     const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexTexture, gv.groupTexture, gv.normalTexture, boundingSphere, textureMesh);
@@ -290,6 +287,7 @@ export function GaussianSurfaceTextureMeshVisual(materialId: number): UnitsVisua
 async function createStructureGaussianSurfaceTextureMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: GaussianDensityProps, textureMesh?: TextureMesh): Promise<TextureMesh> {
     if (!ctx.webgl) throw new Error('webgl context required to create structure gaussian surface texture-mesh');
 
+    if (isTimingMode) ctx.webgl.timer.mark('createStructureGaussianSurfaceTextureMesh');
     const { namedTextures, resources, extensions: { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } } = ctx.webgl;
     if (!namedTextures[GaussianSurfaceName]) {
         namedTextures[GaussianSurfaceName] = colorBufferHalfFloat && textureHalfFloat
@@ -299,18 +297,13 @@ async function createStructureGaussianSurfaceTextureMesh(ctx: VisualContext, str
                 : resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
     }
 
-    // console.time('computeUnitGaussianDensityTexture2d');
     const densityTextureData = await computeStructureGaussianDensityTexture2d(structure, theme.size, true, props, ctx.webgl, namedTextures[GaussianSurfaceName]).runInContext(ctx.runtime);
-    // console.log(densityTextureData);
-    // console.log('vertexGroupTexture', readTexture(ctx.webgl, densityTextureData.texture));
-    // ctx.webgl.waitForGpuCommandsCompleteSync();
-    // console.timeEnd('computeUnitGaussianDensityTexture2d');
-
     const isoLevel = Math.exp(-props.smoothness) / densityTextureData.radiusFactor;
 
     const axisOrder = Vec3.create(0, 1, 2);
     const buffer = textureMesh?.doubleBuffer.get();
     const gv = extractIsosurface(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridTexScale, densityTextureData.transform, isoLevel, false, true, axisOrder, buffer?.vertex, buffer?.group, buffer?.normal);
+    if (isTimingMode) ctx.webgl.timer.markEnd('createStructureGaussianSurfaceTextureMesh');
 
     const boundingSphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, densityTextureData.maxRadius);
     const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexTexture, gv.groupTexture, gv.normalTexture, boundingSphere, textureMesh);

+ 11 - 2
src/mol-util/debug.ts

@@ -30,7 +30,12 @@ let isDebugMode = function getIsDebug() {
     }
 }();
 
-export { isProductionMode, isDebugMode };
+/**
+ * set to true to gather timings, mostly used in `mol-gl`
+ */
+let isTimingMode = false;
+
+export { isProductionMode, isDebugMode, isTimingMode };
 
 export function setProductionMode(value?: boolean) {
     if (typeof value !== 'undefined') isProductionMode = value;
@@ -38,4 +43,8 @@ export function setProductionMode(value?: boolean) {
 
 export function setDebugMode(value?: boolean) {
     if (typeof value !== 'undefined') isDebugMode = value;
-}
+}
+
+export function setTimingMode(value?: boolean) {
+    if (typeof value !== 'undefined') isTimingMode = value;
+}