Commit 25756bb5 authored by Alexei Filippov's avatar Alexei Filippov Committed by Commit Bot

DevTools: Utilize WebGL to draw flamechart bars.

This speeds up bars drawing by an order of magnitude as all the work is
delegated to the GPU.

Vertex layout is performed once when model is loaded or when groups are
expanded/collapsed. The actual drawing caused by zooming and panning the
flamechart just modifies the transformation matrices.

Titles and custom decorations are still drawn on the overlay 2D canvas.

The WebGL mode is currently put behind an experiment.

BUG=874116

Change-Id: I5984db2769d8ed5317b630bd705ab8447baa9358
Reviewed-on: https://chromium-review.googlesource.com/1174861
Commit-Queue: Alexei Filippov <alph@chromium.org>
Reviewed-by: default avatarAleksey Kozyatinskiy <kozyatinskiy@chromium.org>
Cr-Commit-Position: refs/heads/master@{#583025}
parent 6035f138
...@@ -131,6 +131,7 @@ Main.Main = class { ...@@ -131,6 +131,7 @@ Main.Main = class {
Runtime.experiments.register('timelineShowAllEvents', 'Timeline: show all events', true); Runtime.experiments.register('timelineShowAllEvents', 'Timeline: show all events', true);
Runtime.experiments.register('timelineTracingJSProfile', 'Timeline: tracing based JS profiler', true); Runtime.experiments.register('timelineTracingJSProfile', 'Timeline: tracing based JS profiler', true);
Runtime.experiments.register('timelineV8RuntimeCallStats', 'Timeline: V8 Runtime Call Stats on Timeline', true); Runtime.experiments.register('timelineV8RuntimeCallStats', 'Timeline: V8 Runtime Call Stats on Timeline', true);
Runtime.experiments.register('timelineWebGL', 'Timeline: WebGL-based flamechart');
Runtime.experiments.cleanUpStaleExperiments(); Runtime.experiments.cleanUpStaleExperiments();
......
...@@ -73,13 +73,18 @@ PerfUI.FlameChart = class extends UI.VBox { ...@@ -73,13 +73,18 @@ PerfUI.FlameChart = class extends UI.VBox {
this._groupExpansionState = groupExpansionSetting && groupExpansionSetting.get() || {}; this._groupExpansionState = groupExpansionSetting && groupExpansionSetting.get() || {};
this._flameChartDelegate = flameChartDelegate; this._flameChartDelegate = flameChartDelegate;
this._useWebGL = Runtime.experiments.isEnabled('timelineWebGL');
this._chartViewport = new PerfUI.ChartViewport(this); this._chartViewport = new PerfUI.ChartViewport(this);
this._chartViewport.show(this.contentElement); this._chartViewport.show(this.contentElement);
this._dataProvider = dataProvider; this._dataProvider = dataProvider;
this._viewportElement = this._chartViewport.viewportElement; this._viewportElement = this._chartViewport.viewportElement;
this._canvas = /** @type {!HTMLCanvasElement} */ (this._viewportElement.createChild('canvas')); if (this._useWebGL) {
this._canvasGL = /** @type {!HTMLCanvasElement} */ (this._viewportElement.createChild('canvas', 'fill'));
this._initWebGL();
}
this._canvas = /** @type {!HTMLCanvasElement} */ (this._viewportElement.createChild('canvas', 'fill'));
this._canvas.tabIndex = 0; this._canvas.tabIndex = 0;
this.setDefaultFocusedElement(this._canvas); this.setDefaultFocusedElement(this._canvas);
...@@ -204,6 +209,12 @@ PerfUI.FlameChart = class extends UI.VBox { ...@@ -204,6 +209,12 @@ PerfUI.FlameChart = class extends UI.VBox {
this._canvas.height = height; this._canvas.height = height;
this._canvas.style.width = `${width / ratio}px`; this._canvas.style.width = `${width / ratio}px`;
this._canvas.style.height = `${height / ratio}px`; this._canvas.style.height = `${height / ratio}px`;
if (this._useWebGL) {
this._canvasGL.width = width;
this._canvasGL.height = height;
this._canvasGL.style.width = `${width / ratio}px`;
this._canvasGL.style.height = `${height / ratio}px`;
}
} }
/** /**
...@@ -683,6 +694,8 @@ PerfUI.FlameChart = class extends UI.VBox { ...@@ -683,6 +694,8 @@ PerfUI.FlameChart = class extends UI.VBox {
const ratio = window.devicePixelRatio; const ratio = window.devicePixelRatio;
const top = this._chartViewport.scrollOffset(); const top = this._chartViewport.scrollOffset();
context.scale(ratio, ratio); context.scale(ratio, ratio);
context.fillStyle = 'rgba(0, 0, 0, 0)';
context.fillRect(0, 0, width, height);
context.translate(0, -top); context.translate(0, -top);
const defaultFont = '11px ' + Host.fontFamily(); const defaultFont = '11px ' + Host.fontFamily();
context.font = defaultFont; context.font = defaultFont;
...@@ -696,17 +709,9 @@ PerfUI.FlameChart = class extends UI.VBox { ...@@ -696,17 +709,9 @@ PerfUI.FlameChart = class extends UI.VBox {
const markerIndices = []; const markerIndices = [];
const textPadding = this._textPadding; const textPadding = this._textPadding;
const minTextWidth = 2 * textPadding + UI.measureTextWidth(context, '\u2026'); const minTextWidth = 2 * textPadding + UI.measureTextWidth(context, '\u2026');
const minTextWidthDuration = this._chartViewport.pixelToTimeOffset(minTextWidth);
const minVisibleBarLevel = Math.max(this._visibleLevelOffsets.upperBound(top) - 1, 0); const minVisibleBarLevel = Math.max(this._visibleLevelOffsets.upperBound(top) - 1, 0);
context.save();
this._forEachGroup((offset, index, group, isFirst, groupHeight) => {
if (index === this._selectedGroup) {
context.fillStyle = this._selectedGroupBackroundColor;
context.fillRect(0, offset, width, groupHeight - group.style.padding);
}
});
context.restore();
/** @type {!Map<string, !Array<number>>} */ /** @type {!Map<string, !Array<number>>} */
const colorBuckets = new Map(); const colorBuckets = new Map();
for (let level = minVisibleBarLevel; level < this._dataProvider.maxStackDepth(); ++level) { for (let level = minVisibleBarLevel; level < this._dataProvider.maxStackDepth(); ++level) {
...@@ -724,10 +729,19 @@ PerfUI.FlameChart = class extends UI.VBox { ...@@ -724,10 +729,19 @@ PerfUI.FlameChart = class extends UI.VBox {
let lastDrawOffset = Infinity; let lastDrawOffset = Infinity;
for (let entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) { for (let entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) {
const entryIndex = levelIndexes[entryIndexOnLevel]; const entryIndex = levelIndexes[entryIndexOnLevel];
let duration = entryTotalTimes[entryIndex];
if (isNaN(duration))
markerIndices.push(entryIndex);
duration = duration || 0;
if (duration >= minTextWidthDuration || this._forceDecorationCache[entryIndex])
titleIndices.push(entryIndex);
const entryStartTime = entryStartTimes[entryIndex]; const entryStartTime = entryStartTimes[entryIndex];
const entryOffsetRight = entryStartTime + (entryTotalTimes[entryIndex] || 0); const entryOffsetRight = entryStartTime + duration;
if (entryOffsetRight <= this._chartViewport.windowLeftTime()) if (entryOffsetRight <= this._chartViewport.windowLeftTime())
break; break;
if (this._useWebGL)
continue;
const barX = this._timeToPositionClipped(entryStartTime); const barX = this._timeToPositionClipped(entryStartTime);
// Check if the entry entirely fits into an already drawn pixel, we can just skip drawing it. // Check if the entry entirely fits into an already drawn pixel, we can just skip drawing it.
...@@ -745,6 +759,18 @@ PerfUI.FlameChart = class extends UI.VBox { ...@@ -745,6 +759,18 @@ PerfUI.FlameChart = class extends UI.VBox {
} }
} }
if (this._useWebGL) {
this._drawGL();
} else {
context.save();
this._forEachGroup((offset, index, group, isFirst, groupHeight) => {
if (index === this._selectedGroup) {
context.fillStyle = this._selectedGroupBackroundColor;
context.fillRect(0, offset, width, groupHeight - group.style.padding);
}
});
context.restore();
const colors = colorBuckets.keysArray(); const colors = colorBuckets.keysArray();
// We don't use for-of here because it's slow. // We don't use for-of here because it's slow.
for (let c = 0; c < colors.length; ++c) { for (let c = 0; c < colors.length; ++c) {
...@@ -762,21 +788,19 @@ PerfUI.FlameChart = class extends UI.VBox { ...@@ -762,21 +788,19 @@ PerfUI.FlameChart = class extends UI.VBox {
if (isNaN(duration)) { if (isNaN(duration)) {
context.moveTo(barX + this._markerRadius, barY + barHeight / 2); context.moveTo(barX + this._markerRadius, barY + barHeight / 2);
context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2); context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2);
markerIndices.push(entryIndex);
continue; continue;
} }
const barRight = this._timeToPositionClipped(entryStartTime + duration); const barRight = this._timeToPositionClipped(entryStartTime + duration);
const barWidth = Math.max(barRight - barX, 1); const barWidth = Math.max(barRight - barX, 1);
if (color) if (color)
context.rect(barX, barY, barWidth - 0.4, barHeight - 1); context.rect(barX, barY, barWidth - 0.4, barHeight - 1);
if (barWidth > minTextWidth || this._dataProvider.forceDecoration(entryIndex))
titleIndices.push(entryIndex);
} }
if (!color) if (!color)
continue; continue;
context.fillStyle = color; context.fillStyle = color;
context.fill(); context.fill();
} }
}
context.beginPath(); context.beginPath();
for (let m = 0; m < markerIndices.length; ++m) { for (let m = 0; m < markerIndices.length; ++m) {
...@@ -834,6 +858,174 @@ PerfUI.FlameChart = class extends UI.VBox { ...@@ -834,6 +858,174 @@ PerfUI.FlameChart = class extends UI.VBox {
this._updateMarkerHighlight(); this._updateMarkerHighlight();
} }
_initWebGL() {
const gl = /** @type {?WebGLRenderingContext} */ (this._canvasGL.getContext('webgl'));
if (!gl) {
console.error('Failed to obtain WebGL context.');
this._useWebGL = false; // Fallback to use canvas.
return;
}
const vertexShaderSource = `
attribute vec2 aVertexPosition;
attribute vec4 aVertexColor;
uniform vec2 uScalingFactor;
uniform vec2 uShiftVector;
varying lowp vec4 vColor;
void main() {
vec2 shiftedPosition = aVertexPosition - uShiftVector;
gl_Position = vec4(shiftedPosition * uScalingFactor + vec2(-1.0, 1.0), 0.0, 1.0);
vColor = aVertexColor;
}`;
const fragmentShaderSource = `
varying lowp vec4 vColor;
void main() {
gl_FragColor = vColor;
}`;
/**
* @param {!WebGLRenderingContext} gl
* @param {number} type
* @param {string} source
* @return {?WebGLShader}
*/
function loadShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS))
return shader;
console.error('Shader compile error: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
this._shaderProgram = shaderProgram;
gl.useProgram(shaderProgram);
} else {
console.error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
this._shaderProgram = null;
}
}
_setupGLGeometry() {
const gl = /** @type {?WebGLRenderingContext} */ (this._canvasGL.getContext('webgl'));
if (!gl)
return;
const timelineData = this._timelineData();
if (!timelineData)
return;
const entryTotalTimes = timelineData.entryTotalTimes;
const entryStartTimes = timelineData.entryStartTimes;
const entryLevels = timelineData.entryLevels;
// 2 triangles per bar x 3 points x 2 coordinates = 12.
const vertexArray = new Float32Array(entryTotalTimes.length * 12);
// 2 triangles x 3 points x 4 color values (RGBA) = 24.
const colorArray = new Float32Array(entryTotalTimes.length * 24);
let vertex = 0;
for (let i = 0; i < entryTotalTimes.length; ++i) {
const level = entryLevels[i];
if (!this._visibleLevels[level])
continue;
const color = this._dataProvider.entryColor(i);
if (!color)
continue;
const rgba = Common.Color.parse(color).rgba();
const cpos = vertex * 4;
for (let j = 0; j < 6; ++j) // All of the bar vertices have the same color.
colorArray.set(rgba, cpos + j * 4);
const vpos = vertex * 2;
const x0 = entryStartTimes[i] - this._minimumBoundary;
const x1 = x0 + entryTotalTimes[i];
const y0 = this._levelToOffset(level);
const y1 = y0 + this._levelHeight(level) - 1;
vertexArray[vpos + 0] = x0;
vertexArray[vpos + 1] = y0;
vertexArray[vpos + 2] = x1;
vertexArray[vpos + 3] = y0;
vertexArray[vpos + 4] = x0;
vertexArray[vpos + 5] = y1;
vertexArray[vpos + 6] = x0;
vertexArray[vpos + 7] = y1;
vertexArray[vpos + 8] = x1;
vertexArray[vpos + 9] = y0;
vertexArray[vpos + 10] = x1;
vertexArray[vpos + 11] = y1;
vertex += 6; // vertices per bar.
}
this._vertexCount = vertex;
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW);
const aVertexPosition = gl.getAttribLocation(this._shaderProgram, 'aVertexPosition');
gl.enableVertexAttribArray(aVertexPosition);
gl.vertexAttribPointer(aVertexPosition, /* vertexComponents*/ 2, gl.FLOAT, false, 0, 0);
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW);
const aVertexColor = gl.getAttribLocation(this._shaderProgram, 'aVertexColor');
gl.enableVertexAttribArray(aVertexColor);
gl.vertexAttribPointer(aVertexColor, /* colorComponents*/ 4, gl.FLOAT, false, 0, 0);
}
_drawGL() {
const gl = /** @type {?WebGLRenderingContext} */ (this._canvasGL.getContext('webgl'));
if (!gl)
return;
const timelineData = this._timelineData();
if (!timelineData)
return;
const width = this._canvasGL.width;
const height = this._canvasGL.height;
if (!this._prevTimelineData || timelineData.entryTotalTimes !== this._prevTimelineData.entryTotalTimes) {
this._prevTimelineData = timelineData;
this._setupGLGeometry();
}
const viewportScale = [2.0 / this.boundarySpan(), -2.0 * window.devicePixelRatio / height];
const viewportShift = [this.minimumBoundary() - this.zeroTime(), this._chartViewport.scrollOffset()];
gl.viewport(0, 0, width, height);
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
if (!this._vertexCount)
return;
const uScalingFactor = gl.getUniformLocation(this._shaderProgram, 'uScalingFactor');
const uShiftVector = gl.getUniformLocation(this._shaderProgram, 'uShiftVector');
gl.uniform2fv(uScalingFactor, viewportScale);
gl.uniform2fv(uShiftVector, viewportShift);
gl.drawArrays(gl.TRIANGLES, 0, this._vertexCount);
}
/** /**
* @param {number} width * @param {number} width
* @param {number} height * @param {number} height
...@@ -1197,6 +1389,7 @@ PerfUI.FlameChart = class extends UI.VBox { ...@@ -1197,6 +1389,7 @@ PerfUI.FlameChart = class extends UI.VBox {
this._visibleLevels = null; this._visibleLevels = null;
this._groupOffsets = null; this._groupOffsets = null;
this._rawTimelineData = null; this._rawTimelineData = null;
this._forceDecorationCache = null;
this._rawTimelineDataLength = 0; this._rawTimelineDataLength = 0;
this._selectedGroup = -1; this._selectedGroup = -1;
this._flameChartDelegate.updateSelectedGroup(this, null); this._flameChartDelegate.updateSelectedGroup(this, null);
...@@ -1205,6 +1398,9 @@ PerfUI.FlameChart = class extends UI.VBox { ...@@ -1205,6 +1398,9 @@ PerfUI.FlameChart = class extends UI.VBox {
this._rawTimelineData = timelineData; this._rawTimelineData = timelineData;
this._rawTimelineDataLength = timelineData.entryStartTimes.length; this._rawTimelineDataLength = timelineData.entryStartTimes.length;
this._forceDecorationCache = new Int8Array(this._rawTimelineDataLength);
for (let i = 0; i < this._forceDecorationCache.length; ++i)
this._forceDecorationCache[i] = this._dataProvider.forceDecoration(i) ? 1 : 0;
const entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1); const entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1);
for (let i = 0; i < timelineData.entryLevels.length; ++i) for (let i = 0; i < timelineData.entryLevels.length; ++i)
...@@ -1293,6 +1489,8 @@ PerfUI.FlameChart = class extends UI.VBox { ...@@ -1293,6 +1489,8 @@ PerfUI.FlameChart = class extends UI.VBox {
if (groupIndex >= 0) if (groupIndex >= 0)
this._groupOffsets[groupIndex + 1] = currentOffset; this._groupOffsets[groupIndex + 1] = currentOffset;
this._visibleLevelOffsets[level] = currentOffset; this._visibleLevelOffsets[level] = currentOffset;
if (this._useWebGL)
this._setupGLGeometry();
} }
/** /**
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment