From 80fb531fdba59ba77ae56daff71b2ae8ce6309b9 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 6 Nov 2019 13:16:48 -0500 Subject: [PATCH] add gpu timing map events (#8829) * Introduce 'gpuTiming' map options. Originally implemented by Chris. I've rebased it and exposed it with just the event listeners instead of a map option. Listen to `gpu-timing-frame` to get the gpu time for the frame and listen to `gpu-timing-layer` to get the gpu time for all individual layers. It is not recommended to listen to both. * fixup --- src/gl/context.js | 3 +++ src/render/painter.js | 49 +++++++++++++++++++++++++++++++++++++++++++ src/ui/map.js | 37 ++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/src/gl/context.js b/src/gl/context.js index a5cac49d7bb..8290730025b 100644 --- a/src/gl/context.js +++ b/src/gl/context.js @@ -63,6 +63,7 @@ class Context { extTextureFilterAnisotropic: any; extTextureFilterAnisotropicMax: any; extTextureHalfFloat: any; + extTimerQuery: any; constructor(gl: WebGLRenderingContext) { this.gl = gl; @@ -113,6 +114,8 @@ class Context { if (this.extTextureHalfFloat) { gl.getExtension('OES_texture_half_float_linear'); } + + this.extTimerQuery = gl.getExtension('EXT_disjoint_timer_query'); } setDefault() { diff --git a/src/render/painter.js b/src/render/painter.js index b7cf3fe292e..04e2189825e 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -73,6 +73,7 @@ type PainterOptions = { rotating: boolean, zooming: boolean, moving: boolean, + gpuTiming: boolean, fadeDuration: number } @@ -121,6 +122,7 @@ class Painter { cache: { [string]: Program<*> }; crossTileSymbolIndex: CrossTileSymbolIndex; symbolFadeChange: number; + gpuTimers: { [string]: any }; constructor(gl: WebGLRenderingContext, transform: Transform) { this.context = new Context(gl); @@ -139,6 +141,8 @@ class Painter { this.emptyProgramConfiguration = new ProgramConfiguration(); this.crossTileSymbolIndex = new CrossTileSymbolIndex(); + + this.gpuTimers = {}; } /* @@ -469,7 +473,52 @@ class Painter { if (layer.type !== 'background' && layer.type !== 'custom' && !coords.length) return; this.id = layer.id; + this.gpuTimingStart(layer); draw[layer.type](painter, sourceCache, layer, coords, this.style.placement.variableOffsets); + this.gpuTimingEnd(); + } + + gpuTimingStart(layer: StyleLayer) { + if (!this.options.gpuTiming) return; + const ext = this.context.extTimerQuery; + // This tries to time the draw call itself, but note that the cost for drawing a layer + // may be dominated by the cost of uploading vertices to the GPU. + // To instrument that, we'd need to pass the layerTimers object down into the bucket + // uploading logic. + let layerTimer = this.gpuTimers[layer.id]; + if (!layerTimer) { + layerTimer = this.gpuTimers[layer.id] = { + calls: 0, + cpuTime: 0, + query: ext.createQueryEXT() + }; + } + layerTimer.calls++; + ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, layerTimer.query); + } + + gpuTimingEnd() { + if (!this.options.gpuTiming) return; + const ext = this.context.extTimerQuery; + ext.endQueryEXT(ext.TIME_ELAPSED_EXT); + } + + collectGpuTimers() { + const currentLayerTimers = this.gpuTimers; + this.gpuTimers = {}; + return currentLayerTimers; + } + + queryGpuTimers(gpuTimers: {[string]: any}) { + const layers = {}; + for (const layerId in gpuTimers) { + const gpuTimer = gpuTimers[layerId]; + const ext = this.context.extTimerQuery; + const gpuTime = ext.getQueryObjectEXT(gpuTimer.query, ext.QUERY_RESULT_EXT) / (1000 * 1000); + ext.deleteQueryEXT(gpuTimer.query); + layers[layerId] = gpuTime; + } + return layers; } /** diff --git a/src/ui/map.js b/src/ui/map.js index f744f6abf21..2427740485a 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -1938,6 +1938,14 @@ class Map extends Camera { * @private */ _render() { + let gpuTimer, frameStartTime = 0; + const extTimerQuery = this.painter.context.extTimerQuery; + if (this.listens('gpu-timing-frame')) { + gpuTimer = extTimerQuery.createQueryEXT(); + extTimerQuery.beginQueryEXT(extTimerQuery.TIME_ELAPSED_EXT, gpuTimer); + frameStartTime = browser.now(); + } + // A custom layer may have used the context asynchronously. Mark the state as dirty. this.painter.context.setDirty(); this.painter.setBaseState(); @@ -1989,6 +1997,7 @@ class Map extends Camera { rotating: this.isRotating(), zooming: this.isZooming(), moving: this.isMoving(), + gpuTiming: !!this.listens('gpu-timing-layer'), fadeDuration: this._fadeDuration }); @@ -2010,6 +2019,33 @@ class Map extends Camera { this.style._releaseSymbolFadeTiles(); } + if (this.listens('gpu-timing-frame')) { + const renderCPUTime = browser.now() - frameStartTime; + extTimerQuery.endQueryEXT(extTimerQuery.TIME_ELAPSED_EXT, gpuTimer); + setTimeout(() => { + const renderGPUTime = extTimerQuery.getQueryObjectEXT(gpuTimer, extTimerQuery.QUERY_RESULT_EXT) / (1000 * 1000); + extTimerQuery.deleteQueryEXT(gpuTimer); + this.fire(new Event('gpu-timing-frame', { + cpuTime: renderCPUTime, + gpuTime: renderGPUTime + })); + }, 50); // Wait 50ms to give time for all GPU calls to finish before querying + } + + if (this.listens('gpu-timing-layer')) { + // Resetting the Painter's per-layer timing queries here allows us to isolate + // the queries to individual frames. + const frameLayerQueries = this.painter.collectGpuTimers(); + + setTimeout(() => { + const renderedLayerTimes = this.painter.queryGpuTimers(frameLayerQueries); + + this.fire(new Event('gpu-timing-layer', { + layerTimes: renderedLayerTimes + })); + }, 50); // Wait 50ms to give time for all GPU calls to finish before querying + } + // Schedule another render frame if it's needed. // // Even though `_styleDirty` and `_sourcesDirty` are reset in this @@ -2020,6 +2056,7 @@ class Map extends Camera { } else if (!this.isMoving() && this.loaded()) { this.fire(new Event('idle')); } + return this; }