Skip to content

Commit 80fb531

Browse files
ansismourner
authored andcommitted
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
1 parent 94f5a36 commit 80fb531

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

src/gl/context.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class Context {
6363
extTextureFilterAnisotropic: any;
6464
extTextureFilterAnisotropicMax: any;
6565
extTextureHalfFloat: any;
66+
extTimerQuery: any;
6667

6768
constructor(gl: WebGLRenderingContext) {
6869
this.gl = gl;
@@ -113,6 +114,8 @@ class Context {
113114
if (this.extTextureHalfFloat) {
114115
gl.getExtension('OES_texture_half_float_linear');
115116
}
117+
118+
this.extTimerQuery = gl.getExtension('EXT_disjoint_timer_query');
116119
}
117120

118121
setDefault() {

src/render/painter.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type PainterOptions = {
7373
rotating: boolean,
7474
zooming: boolean,
7575
moving: boolean,
76+
gpuTiming: boolean,
7677
fadeDuration: number
7778
}
7879

@@ -121,6 +122,7 @@ class Painter {
121122
cache: { [string]: Program<*> };
122123
crossTileSymbolIndex: CrossTileSymbolIndex;
123124
symbolFadeChange: number;
125+
gpuTimers: { [string]: any };
124126

125127
constructor(gl: WebGLRenderingContext, transform: Transform) {
126128
this.context = new Context(gl);
@@ -139,6 +141,8 @@ class Painter {
139141
this.emptyProgramConfiguration = new ProgramConfiguration();
140142

141143
this.crossTileSymbolIndex = new CrossTileSymbolIndex();
144+
145+
this.gpuTimers = {};
142146
}
143147

144148
/*
@@ -469,7 +473,52 @@ class Painter {
469473
if (layer.type !== 'background' && layer.type !== 'custom' && !coords.length) return;
470474
this.id = layer.id;
471475

476+
this.gpuTimingStart(layer);
472477
draw[layer.type](painter, sourceCache, layer, coords, this.style.placement.variableOffsets);
478+
this.gpuTimingEnd();
479+
}
480+
481+
gpuTimingStart(layer: StyleLayer) {
482+
if (!this.options.gpuTiming) return;
483+
const ext = this.context.extTimerQuery;
484+
// This tries to time the draw call itself, but note that the cost for drawing a layer
485+
// may be dominated by the cost of uploading vertices to the GPU.
486+
// To instrument that, we'd need to pass the layerTimers object down into the bucket
487+
// uploading logic.
488+
let layerTimer = this.gpuTimers[layer.id];
489+
if (!layerTimer) {
490+
layerTimer = this.gpuTimers[layer.id] = {
491+
calls: 0,
492+
cpuTime: 0,
493+
query: ext.createQueryEXT()
494+
};
495+
}
496+
layerTimer.calls++;
497+
ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, layerTimer.query);
498+
}
499+
500+
gpuTimingEnd() {
501+
if (!this.options.gpuTiming) return;
502+
const ext = this.context.extTimerQuery;
503+
ext.endQueryEXT(ext.TIME_ELAPSED_EXT);
504+
}
505+
506+
collectGpuTimers() {
507+
const currentLayerTimers = this.gpuTimers;
508+
this.gpuTimers = {};
509+
return currentLayerTimers;
510+
}
511+
512+
queryGpuTimers(gpuTimers: {[string]: any}) {
513+
const layers = {};
514+
for (const layerId in gpuTimers) {
515+
const gpuTimer = gpuTimers[layerId];
516+
const ext = this.context.extTimerQuery;
517+
const gpuTime = ext.getQueryObjectEXT(gpuTimer.query, ext.QUERY_RESULT_EXT) / (1000 * 1000);
518+
ext.deleteQueryEXT(gpuTimer.query);
519+
layers[layerId] = gpuTime;
520+
}
521+
return layers;
473522
}
474523

475524
/**

src/ui/map.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1938,6 +1938,14 @@ class Map extends Camera {
19381938
* @private
19391939
*/
19401940
_render() {
1941+
let gpuTimer, frameStartTime = 0;
1942+
const extTimerQuery = this.painter.context.extTimerQuery;
1943+
if (this.listens('gpu-timing-frame')) {
1944+
gpuTimer = extTimerQuery.createQueryEXT();
1945+
extTimerQuery.beginQueryEXT(extTimerQuery.TIME_ELAPSED_EXT, gpuTimer);
1946+
frameStartTime = browser.now();
1947+
}
1948+
19411949
// A custom layer may have used the context asynchronously. Mark the state as dirty.
19421950
this.painter.context.setDirty();
19431951
this.painter.setBaseState();
@@ -1989,6 +1997,7 @@ class Map extends Camera {
19891997
rotating: this.isRotating(),
19901998
zooming: this.isZooming(),
19911999
moving: this.isMoving(),
2000+
gpuTiming: !!this.listens('gpu-timing-layer'),
19922001
fadeDuration: this._fadeDuration
19932002
});
19942003

@@ -2010,6 +2019,33 @@ class Map extends Camera {
20102019
this.style._releaseSymbolFadeTiles();
20112020
}
20122021

2022+
if (this.listens('gpu-timing-frame')) {
2023+
const renderCPUTime = browser.now() - frameStartTime;
2024+
extTimerQuery.endQueryEXT(extTimerQuery.TIME_ELAPSED_EXT, gpuTimer);
2025+
setTimeout(() => {
2026+
const renderGPUTime = extTimerQuery.getQueryObjectEXT(gpuTimer, extTimerQuery.QUERY_RESULT_EXT) / (1000 * 1000);
2027+
extTimerQuery.deleteQueryEXT(gpuTimer);
2028+
this.fire(new Event('gpu-timing-frame', {
2029+
cpuTime: renderCPUTime,
2030+
gpuTime: renderGPUTime
2031+
}));
2032+
}, 50); // Wait 50ms to give time for all GPU calls to finish before querying
2033+
}
2034+
2035+
if (this.listens('gpu-timing-layer')) {
2036+
// Resetting the Painter's per-layer timing queries here allows us to isolate
2037+
// the queries to individual frames.
2038+
const frameLayerQueries = this.painter.collectGpuTimers();
2039+
2040+
setTimeout(() => {
2041+
const renderedLayerTimes = this.painter.queryGpuTimers(frameLayerQueries);
2042+
2043+
this.fire(new Event('gpu-timing-layer', {
2044+
layerTimes: renderedLayerTimes
2045+
}));
2046+
}, 50); // Wait 50ms to give time for all GPU calls to finish before querying
2047+
}
2048+
20132049
// Schedule another render frame if it's needed.
20142050
//
20152051
// Even though `_styleDirty` and `_sourcesDirty` are reset in this
@@ -2020,6 +2056,7 @@ class Map extends Camera {
20202056
} else if (!this.isMoving() && this.loaded()) {
20212057
this.fire(new Event('idle'));
20222058
}
2059+
20232060
return this;
20242061
}
20252062

0 commit comments

Comments
 (0)