diff --git a/application.tsx b/application.tsx index 6352ca1..57385f0 100644 --- a/application.tsx +++ b/application.tsx @@ -1,16 +1,19 @@ import {h} from 'preact' +import * as regl from 'regl' import {StyleSheet, css} from 'aphrodite' import {ReloadableComponent} from './reloadable' import {importFromBGFlameGraph} from './import/bg-flamegraph' import {importFromStackprof} from './import/stackprof' import {importFromChromeTimeline, importFromChromeCPUProfile} from './import/chrome' +import { RectangleBatch, RectangleBatchRenderer } from './rectangle-batch-renderer' import {Profile, Frame} from './profile' import {Flamechart} from './flamechart' import { FlamechartView } from './flamechart-view' import { FontFamily, FontSize, Colors } from './style' import { FrameColorGenerator } from './color' +import { Vec2, Rect } from './math' const enum SortOrder { CHRONO, @@ -20,7 +23,9 @@ const enum SortOrder { interface ApplicationState { profile: Profile | null flamechart: Flamechart | null + flamechartRectBatch: RectangleBatch | null sortedFlamechart: Flamechart | null + sortedFlamechartRectBatch: RectangleBatch | null sortOrder: SortOrder loading: boolean } @@ -118,6 +123,85 @@ export class Toolbar extends ReloadableComponent { } } +interface GLCanvasProps { + setGL(gl: regl.Instance | null): void +} +export class GLCanvas extends ReloadableComponent { + private canvas: HTMLCanvasElement | null + private gl: regl.Instance | null + + private ref = (canvas?: Element) => { + if (canvas instanceof HTMLCanvasElement) { + this.canvas = canvas + this.gl = regl(canvas) + } else { + this.gl = null + } + this.props.setGL(this.gl) + } + + private maybeResize() { + if (!this.canvas || !this.gl) return + let { width, height } = this.canvas.getBoundingClientRect() + width = Math.floor(width) * window.devicePixelRatio + height = Math.floor(height) * window.devicePixelRatio + + // Still initializing: don't resize yet + if (width < 4 || height < 4) return + const oldWidth = this.canvas.width + const oldHeight = this.canvas.height + + // Already at the right size + if (width === oldWidth && height === oldHeight) return + + this.canvas.width = width + this.canvas.height = height + } + + onWindowResize = () => { + this.maybeResize() + window.addEventListener('resize', this.onWindowResize) + } + componentDidMount() { + window.addEventListener('resize', this.onWindowResize) + requestAnimationFrame(() => this.maybeResize()) + } + componentWillUnmount() { + window.removeEventListener('resize', this.onWindowResize) + } + + render() { + return + } +} + +function rectangleBatchForFlamechart(gl: regl.Instance, flamechart: Flamechart) { + console.time('rectangle batch generation') + + const batch = new RectangleBatch(gl) + + const layers = flamechart.getLayers() + for (let i = 0; i < layers.length; i++) { + const layer = layers[i] + for (let flamechartFrame of layer) { + const configSpaceBounds = new Rect( + new Vec2(flamechartFrame.start, i + 1), + new Vec2(flamechartFrame.end - flamechartFrame.start, 1) + ) + const color = flamechart.getColorForFrame(flamechartFrame.node.frame) + batch.addRect(configSpaceBounds, color) + } + } + + // GPU upload + batch.uploadToGPU() + + console.timeEnd('rectangle batch generation') + + return batch +} + + export class Application extends ReloadableComponent<{}, ApplicationState> { constructor() { super() @@ -125,12 +209,16 @@ export class Application extends ReloadableComponent<{}, ApplicationState> { loading: false, profile: null, flamechart: null, + flamechartRectBatch: null, sortedFlamechart: null, + sortedFlamechartRectBatch: null, sortOrder: SortOrder.CHRONO } } loadFromString(fileName: string, contents: string) { + if (!this.gl) return + console.time('import') const profile = importProfile(contents, fileName) if (profile == null) { @@ -153,6 +241,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> { formatValue: profile.formatValue.bind(profile), getColorForFrame: colorGenerator.getColorForFrame.bind(colorGenerator) }) + const flamechartRectBatch = rectangleBatchForFlamechart(this.gl, flamechart) const sortedFlamechart = new Flamechart({ getTotalWeight: profile.getTotalNonIdleWeight.bind(profile), @@ -160,10 +249,19 @@ export class Application extends ReloadableComponent<{}, ApplicationState> { formatValue: profile.formatValue.bind(profile), getColorForFrame: colorGenerator.getColorForFrame.bind(colorGenerator) }) + const sortedFlamechartRectBatch = rectangleBatchForFlamechart(this.gl, sortedFlamechart) + console.timeEnd('import') console.time('first setState') - this.setState({ profile, flamechart, sortedFlamechart, loading: false }, () => { + this.setState({ + profile, + flamechart, + flamechartRectBatch, + sortedFlamechart, + sortedFlamechartRectBatch, + loading: false + }, () => { console.timeEnd('first setState') }) } @@ -263,22 +361,48 @@ export class Application extends ReloadableComponent<{}, ApplicationState> { this.setState({ sortOrder }) } + private gl: regl.Instance | null = null + private rectangleBatchRenderer: RectangleBatchRenderer | null = null + private setGL = (gl: regl.Instance | null) => { + if (gl) { + this.gl = gl + this.rectangleBatchRenderer = new RectangleBatchRenderer(gl) + } else { + this.gl = null + this.rectangleBatchRenderer = null + } + } + render() { - const {flamechart, sortedFlamechart, sortOrder, loading} = this.state + const {flamechart, flamechartRectBatch, sortedFlamechart, sortedFlamechartRectBatch, sortOrder, loading} = this.state const flamechartToView = sortOrder == SortOrder.CHRONO ? flamechart : sortedFlamechart + const rectangleBatch = sortOrder == SortOrder.CHRONO ? flamechartRectBatch : sortedFlamechartRectBatch return
+ {loading ? this.renderLoadingBar() : - flamechartToView ? - : + this.gl && flamechartToView && this.rectangleBatchRenderer && rectangleBatch ? + : this.renderLanding()}
} } const style = StyleSheet.create({ + glCanvasView: { + position: 'absolute', + width: '100vw', + height: '100vh', + zIndex: -1, + pointerEvents: 'none' + }, loading: { height: 3, marginBottom: -3, diff --git a/flamechart-minimap-view.tsx b/flamechart-minimap-view.tsx index 6df9ea7..2e726e1 100644 --- a/flamechart-minimap-view.tsx +++ b/flamechart-minimap-view.tsx @@ -1,10 +1,10 @@ import * as regl from 'regl' -import { vec3, Command } from 'regl' +import { Command } from 'regl' import { h, Component } from 'preact' import { css } from 'aphrodite' import { Flamechart } from './flamechart' import { Rect, Vec2, AffineTransform, clamp } from './math' -import { rectangleBatchRenderer, RectangleBatchRendererProps } from "./rectangle-batch-renderer" +import { RectangleBatchRenderer, RectangleBatch } from "./rectangle-batch-renderer" import { atMostOnceAFrame, cachedMeasureTextWidth } from "./utils"; import { style, Sizes } from "./flamechart-style"; import { FontFamily, FontSize, Colors } from "./style" @@ -14,6 +14,13 @@ const DEVICE_PIXEL_RATIO = window.devicePixelRatio interface FlamechartMinimapViewProps { flamechart: Flamechart configSpaceViewportRect: Rect + + // TODO(jlfwong): Encapsulate the regl.Instance and the batch renderer + // in a CanvasContext object or something along those lines + gl: regl.Instance + renderer: RectangleBatchRenderer + rectangles: RectangleBatch + transformViewport: (transform: AffineTransform) => void setConfigSpaceViewportRect: (rect: Rect) => void } @@ -24,11 +31,8 @@ enum DraggingMode { } export class FlamechartMinimapView extends Component { - renderer: Command | null = null viewportRectRenderer: Command | null = null - ctx: WebGLRenderingContext | null = null - gl: regl.Instance | null = null canvas: HTMLCanvasElement | null = null overlayCanvas: HTMLCanvasElement | null = null @@ -77,21 +81,39 @@ export class FlamechartMinimapView extends Component { + this.props.renderer.render({ + configSpaceToNDC: configSpaceToNDC, + physicalSize: this.physicalViewSize(), + strokeSize: 0, + batch: this.props.rectangles + }) }) + /* this.viewportRectRenderer({ configSpaceViewportRect: this.props.configSpaceViewportRect, configSpaceToPhysicalViewSpace: this.configSpaceToPhysicalViewSpace(), physicalSize: this.physicalViewSize() }) + */ } private renderOverlays() { @@ -152,7 +174,6 @@ export class FlamechartMinimapView extends Component this.renderCanvas()) } else { - if (!this.gl) return; - if (!this.renderer) this.preprocess(this.props.flamechart) - this.gl.clear({ - color: [1, 1, 1, 1], - depth: 1 - }) this.renderRects() this.renderOverlays() } @@ -225,8 +238,6 @@ export class FlamechartMinimapView extends Component { if (element) { this.canvas = element as HTMLCanvasElement - this.ctx = this.canvas.getContext('webgl')! - this.gl = regl(this.ctx) this.renderCanvas() } else { this.canvas = null @@ -410,31 +421,6 @@ export class FlamechartMinimapView extends Component { if (element) { this.overlayCanvas = element as HTMLCanvasElement diff --git a/flamechart-view.tsx b/flamechart-view.tsx index ea34bf6..e76c087 100644 --- a/flamechart-view.tsx +++ b/flamechart-view.tsx @@ -1,3 +1,4 @@ +import * as regl from 'regl' import {h} from 'preact' import {css} from 'aphrodite' import {ReloadableComponent} from './reloadable' @@ -5,12 +6,9 @@ import {ReloadableComponent} from './reloadable' import { CallTreeNode } from './profile' import { Flamechart, FlamechartFrame } from './flamechart' -import * as regl from 'regl' -import { vec3, Command, Instance } from 'regl' - import { Rect, Vec2, AffineTransform, clamp } from './math' import { atMostOnceAFrame, cachedMeasureTextWidth } from "./utils"; -import { rectangleBatchRenderer, RectangleBatchRendererProps } from "./rectangle-batch-renderer" +import { RectangleBatchRenderer, RectangleBatch } from "./rectangle-batch-renderer" import { FlamechartMinimapView } from "./flamechart-minimap-view" import { style, Sizes } from './flamechart-style' @@ -74,6 +72,11 @@ const DEVICE_PIXEL_RATIO = window.devicePixelRatio */ interface FlamechartPanZoomViewProps { flamechart: Flamechart + + gl: regl.Instance + renderer: RectangleBatchRenderer + rectangles: RectangleBatch + setNodeHover: (node: CallTreeNode | null, logicalViewSpaceMouse: Vec2) => void configSpaceViewportRect: Rect transformViewport: (transform: AffineTransform) => void @@ -81,10 +84,6 @@ interface FlamechartPanZoomViewProps { } export class FlamechartPanZoomView extends ReloadableComponent { - renderer: Command | null = null - - ctx: WebGLRenderingContext | null = null - gl: Instance | null = null canvas: HTMLCanvasElement | null = null overlayCanvas: HTMLCanvasElement | null = null @@ -96,38 +95,9 @@ export class FlamechartPanZoomView extends ReloadableComponent { if (element) { this.canvas = element as HTMLCanvasElement - this.ctx = this.canvas.getContext('webgl')! - this.gl = regl(this.ctx) this.renderCanvas() } else { this.canvas = null @@ -319,7 +289,7 @@ export class FlamechartPanZoomView extends ReloadableComponent { @@ -361,17 +329,33 @@ export class FlamechartPanZoomView extends ReloadableComponent { + this.props.renderer.render({ + configSpaceToNDC: configSpaceToNDC, + physicalSize: this.physicalViewSize(), + strokeSize: 1, + batch: this.props.rectangles + }) }) } @@ -411,7 +395,6 @@ export class FlamechartPanZoomView extends ReloadableComponent this.renderCanvas()) } else { - if (!this.renderer) this.preprocess(this.props.flamechart) this.renderRects() this.renderOverlays() } @@ -584,7 +567,6 @@ export class FlamechartPanZoomView extends ReloadableComponent { - const positions = new Float32Array(rects.length * 6 * 2) - const physicalSpaceOffsets = new Float32Array(rects.length * 6 * 2) - const vertexColors = new Float32Array(rects.length * 6 * 3) + private offset = { + topLeft: new Vec2(1, -1), + topRight: new Vec2(-1, -1), + bottomRight: new Vec2(-1, 1), + bottomLeft: new Vec2(1, 1) + } + constructor(private gl: regl.Instance) { } - const offset = { - topLeft: new Vec2(strokeSize, -strokeSize), - topRight: new Vec2(-strokeSize, -strokeSize), - bottomRight: new Vec2(-strokeSize, strokeSize), - bottomLeft: new Vec2(strokeSize, strokeSize) + getVertexCount() { return this.vertexCount } + + private positionBuffer: regl.Buffer | null = null + getPositionBuffer() { + if (!this.positionBuffer) this.positionBuffer = this.gl.buffer(this.positions) + return this.positionBuffer } - let vertexIndex = 0 - function addVertex(y: number, x: number, offset: Vec2, color: vec3) { - const index = vertexIndex++ - positions[index * 2] = x - positions[index * 2 + 1] = y - physicalSpaceOffsets[index * 2] = offset.x - physicalSpaceOffsets[index * 2 + 1] = offset.y - vertexColors[index * 3] = color[0] - vertexColors[index * 3 + 1] = color[1] - vertexColors[index * 3 + 2] = color[2] + private offsetBuffer: any = null + getPhysicalSpaceOffsetBuffer() { + if (!this.offsetBuffer) this.offsetBuffer = this.gl.buffer(this.physicalSpaceOffsets) + return this.offsetBuffer } - for (let i = 0; i < rects.length; i++) { - const r = rects[i] - const color = colors[i] + private colorBuffer: any = null + getVertexColorBuffer() { + if (!this.colorBuffer) this.colorBuffer = this.gl.buffer(this.vertexColors) + return this.colorBuffer + } - const top = r.top() - const bottom = r.bottom() - const left = r.left() - const right = r.right() + uploadToGPU() { + this.getPositionBuffer() + this.getPhysicalSpaceOffsetBuffer() + this.getVertexColorBuffer() + } + + private addVertex(y: number, x: number, offset: Vec2, color: vec3) { + const index = this.vertexCount++ + if (index >= this.vertexCapacity) { + // Not enough capacity, time to resize! We'll double the capacity each time. + this.vertexCapacity *= 2 + const positions = new Float32Array(this.vertexCapacity * 6 * 2) + const physicalSpaceOffsets = new Float32Array(this.vertexCapacity * 6 * 2) + const vertexColors = new Float32Array(this.vertexCapacity * 6 * 3) + + positions.set(this.positions) + physicalSpaceOffsets.set(this.physicalSpaceOffsets) + vertexColors.set(this.vertexColors) + + this.positions = positions + this.physicalSpaceOffsets = physicalSpaceOffsets + this.vertexColors = vertexColors + } + this.positions[index * 2] = x + this.positions[index * 2 + 1] = y + this.physicalSpaceOffsets[index * 2] = offset.x + this.physicalSpaceOffsets[index * 2 + 1] = offset.y + this.vertexColors[index * 3] = color[0] + this.vertexColors[index * 3 + 1] = color[1] + this.vertexColors[index * 3 + 2] = color[2] + } + + addRect(rect: Rect, color: Color) { + const color_: vec3 = [color.r, color.g, color.b] + + const top = rect.top() + const bottom = rect.bottom() + const left = rect.left() + const right = rect.right() // 2 disjoint triangles. // @@ -46,19 +84,31 @@ export const rectangleBatchRenderer = (gl: regl.Instance, rects: Rect[], colors: // | /| // |/ | // 3 +--+ 2 - addVertex(top, left, offset.topLeft, color) - addVertex(bottom, left, offset.bottomLeft, color) - addVertex(top, right, offset.topRight, color) + this.addVertex(top, left, this.offset.topLeft, color_) + this.addVertex(bottom, left, this.offset.bottomLeft, color_) + this.addVertex(top, right, this.offset.topRight, color_) - addVertex(bottom, left, offset.bottomLeft, color) - addVertex(top, right, offset.topRight, color) - addVertex(bottom, right, offset.bottomRight, color) + this.addVertex(bottom, left, this.offset.bottomLeft, color_) + this.addVertex(top, right, this.offset.topRight, color_) + this.addVertex(bottom, right, this.offset.bottomRight, color_) } +} - return gl({ - vert: ` +export interface RectangleBatchRendererProps { + configSpaceToNDC: AffineTransform + physicalSize: Vec2 + strokeSize: number + batch: RectangleBatch +} + +export class RectangleBatchRenderer { + private command: regl.Command + constructor(gl: regl.Instance) { + this.command = gl({ + vert: ` uniform mat3 configSpaceToNDC; uniform vec2 physicalSize; + uniform float strokeSize; attribute vec2 position; attribute vec3 color; attribute vec2 physicalSpaceOffset; @@ -69,15 +119,15 @@ export const rectangleBatchRenderer = (gl: regl.Instance, rects: Rect[], colors: vec2 halfSize = physicalSize / 2.0; vec2 physicalPixelSize = 2.0 / physicalSize; roundedPosition = floor(roundedPosition * halfSize) / halfSize; - gl_Position = vec4(roundedPosition + physicalPixelSize * physicalSpaceOffset, 0, 1); + gl_Position = vec4(roundedPosition + physicalPixelSize * physicalSpaceOffset * strokeSize, 0, 1); } `, - depth: { - enable: false - }, + depth: { + enable: false + }, - frag: ` + frag: ` precision mediump float; varying vec3 vColor; void main() { @@ -85,38 +135,54 @@ export const rectangleBatchRenderer = (gl: regl.Instance, rects: Rect[], colors: } `, - attributes: { - position: { - buffer: gl.buffer(positions), - offset: 0, - stride: 2 * 4, - size: 2 + attributes: { + position: (context, props) => { + return { + buffer: props.batch.getPositionBuffer(), + offset: 0, + stride: 2 * 4, + size: 2 + } + }, + physicalSpaceOffset: (context, props) => { + return { + buffer: props.batch.getPhysicalSpaceOffsetBuffer(), + offset: 0, + stride: 2 * 4, + size: 2 + } + }, + color: (context, props) => { + return { + buffer: props.batch.getVertexColorBuffer(), + offset: 0, + stride: 3 * 4, + size: 3 + } + } }, - physicalSpaceOffset: { - buffer: gl.buffer(physicalSpaceOffsets), - offset: 0, - stride: 2 * 4, - size: 2 + + uniforms: { + configSpaceToNDC: (context, props) => { + return props.configSpaceToNDC.flatten() + }, + physicalSize: (context, props) => { + return props.physicalSize.flatten() + }, + strokeSize: (context, props) => { + return props.strokeSize + } }, - color: { - buffer: gl.buffer(vertexColors), - offset: 0, - stride: 3 * 4, - size: 3 + + primitive: 'triangles', + + count: (context, props) => { + return props.batch.getVertexCount() } - }, + }) + } - uniforms: { - configSpaceToNDC: (context, props) => { - return props.configSpaceToNDC.flatten() - }, - physicalSize: (context, props) => { - return props.physicalSize.flatten() - } - }, - - primitive: 'triangles', - - count: vertexColors.length / 3 - }) + render(props: RectangleBatchRendererProps) { + this.command(props) + } } \ No newline at end of file diff --git a/regl.d.ts b/regl.d.ts index ea61073..ddf19bd 100644 --- a/regl.d.ts +++ b/regl.d.ts @@ -285,7 +285,7 @@ declare module "regl" { uniforms?: { [uniformName: string]: MaybeComputed } - attributes: { [attributeName: string]: MaybeComputed } + attributes?: { [attributeName: string]: MaybeComputed } primitive?: DrawMode