Skip to content

Commit

Permalink
add gpu timing map events (#8829)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ansis authored and mourner committed Nov 6, 2019
1 parent 94f5a36 commit 80fb531
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/gl/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class Context {
extTextureFilterAnisotropic: any;
extTextureFilterAnisotropicMax: any;
extTextureHalfFloat: any;
extTimerQuery: any;

constructor(gl: WebGLRenderingContext) {
this.gl = gl;
Expand Down Expand Up @@ -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() {
Expand Down
49 changes: 49 additions & 0 deletions src/render/painter.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type PainterOptions = {
rotating: boolean,
zooming: boolean,
moving: boolean,
gpuTiming: boolean,
fadeDuration: number
}

Expand Down Expand Up @@ -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);
Expand All @@ -139,6 +141,8 @@ class Painter {
this.emptyProgramConfiguration = new ProgramConfiguration();

this.crossTileSymbolIndex = new CrossTileSymbolIndex();

this.gpuTimers = {};
}

/*
Expand Down Expand Up @@ -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;
}

/**
Expand Down
37 changes: 37 additions & 0 deletions src/ui/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
});

Expand All @@ -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
Expand All @@ -2020,6 +2056,7 @@ class Map extends Camera {
} else if (!this.isMoving() && this.loaded()) {
this.fire(new Event('idle'));
}

return this;
}

Expand Down

0 comments on commit 80fb531

Please sign in to comment.