123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465 |
- import {mat3, vec2} from 'gl-matrix';
- import {Flamegraph} from '../flamegraph';
- import {FlamegraphSearch} from '../flamegraph/flamegraphStateProvider/flamegraphSearch';
- import {FlamegraphTheme} from '../flamegraph/flamegraphTheme';
- import {FlamegraphFrame, getFlamegraphFrameSearchId} from '../flamegraphFrame';
- import {
- createProgram,
- createShader,
- getContext,
- makeProjectionMatrix,
- Rect,
- resizeCanvasToDisplaySize,
- } from '../gl/utils';
- import {fragment, vertex} from './shaders';
- class FlamegraphRenderer {
- canvas: HTMLCanvasElement | null;
- flamegraph: Flamegraph;
- gl: WebGLRenderingContext | null = null;
- program: WebGLProgram | null = null;
- theme: FlamegraphTheme;
- frames: ReadonlyArray<FlamegraphFrame> = [];
- roots: ReadonlyArray<FlamegraphFrame> = [];
- // Vertex and color buffer
- positions: Array<number> = [];
- bounds: Array<number> = [];
- colors: Array<number> = [];
- colorMap: Map<string | number, number[]> = new Map();
- lastDragPosition: vec2 | null = null;
- attributes: {
- a_bounds: number | null;
- a_color: number | null;
- a_position: number | null;
- } = {
- a_position: null,
- a_color: null,
- a_bounds: null,
- };
- uniforms: {
- u_border_width: WebGLUniformLocation | null;
- u_draw_border: WebGLUniformLocation | null;
- u_is_search_result: WebGLUniformLocation | null;
- u_model: WebGLUniformLocation | null;
- u_projection: WebGLUniformLocation | null;
- } = {
- u_border_width: null,
- u_draw_border: null,
- u_is_search_result: null,
- u_model: null,
- u_projection: null,
- };
- options: {
- draw_border: boolean;
- };
- constructor(
- canvas: HTMLCanvasElement,
- flamegraph: Flamegraph,
- theme: FlamegraphTheme,
- options: {draw_border: boolean} = {draw_border: false}
- ) {
- this.flamegraph = flamegraph;
- this.canvas = canvas;
- this.theme = theme;
- this.options = options;
- this.init();
- }
- init(): void {
- const VERTICES = 6;
- const COLOR_COMPONENTS = 4;
- this.colors = new Array(VERTICES * COLOR_COMPONENTS);
- this.frames = [...this.flamegraph.frames];
- this.roots = [...this.flamegraph.root.children];
- // Generate colors for the flamegraph
- const {colorBuffer, colorMap} = this.theme.COLORS.STACK_TO_COLOR(
- this.frames,
- this.theme.COLORS.COLOR_MAP,
- this.theme.COLORS.COLOR_BUCKET
- );
- this.colorMap = colorMap;
- this.colors = colorBuffer;
- this.initCanvasContext();
- this.initVertices();
- this.initShaders();
- }
- initVertices(): void {
- const POSITIONS_PER_PASS = 2;
- const BOUNDS_PER_PASS = 4;
- const VERTICES = 6;
- this.bounds = new Array(VERTICES * BOUNDS_PER_PASS * this.frames.length);
- this.positions = new Array(VERTICES * POSITIONS_PER_PASS * this.frames.length);
- const length = this.frames.length;
- for (let index = 0; index < length; index++) {
- const frame = this.frames[index];
- const x1 = frame.start;
- const x2 = frame.end;
- const y1 = frame.depth;
- const y2 = frame.depth + 1;
- // top left -> top right -> bottom left ->
- // bottom left -> top right -> bottom right
- const positionOffset = index * 12;
- this.positions[positionOffset] = x1;
- this.positions[positionOffset + 1] = y1;
- this.positions[positionOffset + 2] = x2;
- this.positions[positionOffset + 3] = y1;
- this.positions[positionOffset + 4] = x1;
- this.positions[positionOffset + 5] = y2;
- this.positions[positionOffset + 6] = x1;
- this.positions[positionOffset + 7] = y2;
- this.positions[positionOffset + 8] = x2;
- this.positions[positionOffset + 9] = y1;
- this.positions[positionOffset + 10] = x2;
- this.positions[positionOffset + 11] = y2;
- // @TODO check if we can pack bounds across vertex calls,
- // we are allocating 6x the amount of memory here
- const boundsOffset = index * VERTICES * BOUNDS_PER_PASS;
- for (let i = 0; i < VERTICES; i++) {
- const offset = boundsOffset + i * BOUNDS_PER_PASS;
- this.bounds[offset] = x1;
- this.bounds[offset + 1] = y1;
- this.bounds[offset + 2] = x2;
- this.bounds[offset + 3] = y2;
- }
- }
- }
- initCanvasContext(): void {
- if (!this.canvas) {
- throw new Error('Cannot initialize context from null canvas');
- }
- // Setup webgl canvas context
- this.gl = getContext(this.canvas, 'webgl');
- if (!this.gl) {
- throw new Error('Uninitialized WebGL context');
- }
- this.gl.enable(this.gl.BLEND);
- this.gl.blendFuncSeparate(
- this.gl.SRC_ALPHA,
- this.gl.ONE_MINUS_SRC_ALPHA,
- this.gl.ONE,
- this.gl.ONE_MINUS_SRC_ALPHA
- );
- resizeCanvasToDisplaySize(this.canvas);
- }
- initShaders(): void {
- if (!this.gl) {
- throw new Error('Uninitialized WebGL context');
- }
- // @ts-ignore
- this.uniforms = {};
- // @ts-ignore
- this.attributes = {};
- const vertexShader = createShader(this.gl, this.gl.VERTEX_SHADER, vertex());
- const fragmentShader = createShader(
- this.gl,
- this.gl.FRAGMENT_SHADER,
- fragment(this.theme)
- );
- // create program
- this.program = createProgram(this.gl, vertexShader, fragmentShader);
- const uProjectionMatrix = this.gl.getUniformLocation(this.program, 'u_projection');
- const uModelMatrix = this.gl.getUniformLocation(this.program, 'u_model');
- const uIsSearchResult = this.gl.getUniformLocation(
- this.program,
- 'u_is_search_result'
- );
- const uBorderWidth = this.gl.getUniformLocation(this.program, 'u_border_width');
- const uDrawBorder = this.gl.getUniformLocation(this.program, 'u_draw_border');
- if (!uProjectionMatrix) {
- throw new Error('Could not locate u_projection in shader');
- }
- if (!uModelMatrix) {
- throw new Error('Could not locate u_model in shader');
- }
- if (!uIsSearchResult) {
- throw new Error('Could not locate u_is_search_result in shader');
- }
- if (!uBorderWidth) {
- throw new Error('Could not locate u_border_width in shader');
- }
- if (!uDrawBorder) {
- throw new Error('Could not locate u_draw_border in shader');
- }
- this.uniforms.u_projection = uProjectionMatrix;
- this.uniforms.u_model = uModelMatrix;
- this.uniforms.u_is_search_result = uIsSearchResult;
- this.uniforms.u_border_width = uBorderWidth;
- this.uniforms.u_draw_border = uDrawBorder;
- {
- const aColorAttributeLocation = this.gl.getAttribLocation(this.program, 'a_color');
- if (aColorAttributeLocation === -1) {
- throw new Error('Could not locate a_color in shader');
- }
- // attributes get data from buffers
- this.attributes.a_color = aColorAttributeLocation;
- // Init color buffer
- const colorBuffer = this.gl.createBuffer();
- // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = colorBuffer)
- this.gl.bindBuffer(this.gl.ARRAY_BUFFER, colorBuffer);
- this.gl.bufferData(
- this.gl.ARRAY_BUFFER,
- new Float32Array(this.colors),
- this.gl.STATIC_DRAW
- );
- const size = 4;
- const type = this.gl.FLOAT;
- const normalize = false;
- const stride = 0;
- const offset = 0;
- this.gl.vertexAttribPointer(
- aColorAttributeLocation,
- size,
- type,
- normalize,
- stride,
- offset
- );
- // Point to attribute location
- this.gl.enableVertexAttribArray(aColorAttributeLocation);
- }
- {
- // look up where the vertex data needs to go.
- const aPositionAttributeLocation = this.gl.getAttribLocation(
- this.program,
- 'a_position'
- );
- if (aPositionAttributeLocation === -1) {
- throw new Error('Could not locate a_color in shader');
- }
- // attributes get data from buffers
- this.attributes.a_position = aPositionAttributeLocation;
- // Init position buffer
- const positionBuffer = this.gl.createBuffer();
- // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
- this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer);
- this.gl.bufferData(
- this.gl.ARRAY_BUFFER,
- new Float32Array(this.positions),
- this.gl.STATIC_DRAW
- );
- const size = 2;
- const type = this.gl.FLOAT;
- const normalize = false;
- const stride = 0;
- const offset = 0;
- this.gl.vertexAttribPointer(
- aPositionAttributeLocation,
- size,
- type,
- normalize,
- stride,
- offset
- );
- // Point to attribute location
- this.gl.enableVertexAttribArray(aPositionAttributeLocation);
- }
- {
- // look up where the bounds vertices needs to go.
- const aBoundsAttributeLocation = this.gl.getAttribLocation(
- this.program,
- 'a_bounds'
- );
- if (aBoundsAttributeLocation === -1) {
- throw new Error('Could not locate a_color in shader');
- }
- // attributes get data from buffers
- this.attributes.a_bounds = aBoundsAttributeLocation;
- // Init bounds buffer
- const boundsBuffer = this.gl.createBuffer();
- // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = boundsBuffer)
- this.gl.bindBuffer(this.gl.ARRAY_BUFFER, boundsBuffer);
- this.gl.bufferData(
- this.gl.ARRAY_BUFFER,
- new Float32Array(this.bounds),
- this.gl.STATIC_DRAW
- );
- const size = 4;
- const type = this.gl.FLOAT;
- const normalize = false;
- const stride = 0;
- const offset = 0;
- this.gl.vertexAttribPointer(
- aBoundsAttributeLocation,
- size,
- type,
- normalize,
- stride,
- offset
- );
- // Point to attribute location
- this.gl.enableVertexAttribArray(aBoundsAttributeLocation);
- }
- // Use shader program
- this.gl.useProgram(this.program);
- }
- getColorForFrame(frame: FlamegraphFrame): number[] {
- return this.colorMap.get(frame.key) ?? this.theme.COLORS.FRAME_FALLBACK_COLOR;
- }
- getHoveredNode(configSpaceCursor: vec2): FlamegraphFrame | null {
- let hoveredNode: FlamegraphFrame | null = null;
- const findHoveredNode = (frame: FlamegraphFrame, depth: number) => {
- // This is outside
- if (hoveredNode) {
- return;
- }
- const frameRect = new Rect(frame.start, frame.depth, frame.end - frame.start, 1);
- // We treat entire flamegraph as a segment tree, this allows us to query in O(log n) time by
- // only looking at the nodes that are relevant to the current cursor position. We discard any values
- // on x axis that do not overlap the cursor, and descend until we find a node that overlaps at cursor y position
- if (!frameRect.containsX(configSpaceCursor)) {
- return;
- }
- // If our frame depth overlaps cursor y position, we have found our node
- if (frameRect.containsY(configSpaceCursor)) {
- hoveredNode = frame;
- return;
- }
- // Descend into the rest of the children
- for (let i = 0; i < frame.children.length; i++) {
- findHoveredNode(frame.children[i], depth + 1);
- }
- };
- for (let i = 0; i < this.roots.length; i++) {
- findHoveredNode(this.roots[i], 0);
- }
- return hoveredNode;
- }
- draw(
- configViewToPhysicalSpace: mat3,
- searchResults: FlamegraphSearch['results']
- ): void {
- if (!this.gl) {
- throw new Error('Uninitialized WebGL context');
- }
- this.gl.clearColor(0, 0, 0, 0);
- this.gl.clear(this.gl.COLOR_BUFFER_BIT);
- // We have no frames to draw
- if (!this.positions.length) {
- return;
- }
- this.gl.useProgram(this.program);
- const projectionMatrix = makeProjectionMatrix(
- this.gl.canvas.width,
- this.gl.canvas.height
- );
- // Projection matrix
- this.gl.uniformMatrix3fv(this.uniforms.u_projection, false, projectionMatrix);
- // Model to projection
- this.gl.uniformMatrix3fv(this.uniforms.u_model, false, configViewToPhysicalSpace);
- // Check if we should draw border
- this.gl.uniform1i(this.uniforms.u_draw_border, this.options.draw_border ? 1 : 0);
- // Tell webgl to convert clip space to px
- this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
- const physicalSpacePixel = new Rect(0, 0, 1, 1);
- const physicalToConfig = mat3.invert(mat3.create(), configViewToPhysicalSpace);
- const configSpacePixel = physicalSpacePixel.transformRect(physicalToConfig);
- this.gl.uniform2f(
- this.uniforms.u_border_width,
- configSpacePixel.width,
- configSpacePixel.height
- );
- const VERTICES = 6;
- const length = this.frames.length;
- let frame: FlamegraphFrame | null;
- // This is an optimization to avoid setting uniform1i for each draw call when user is not searching
- if (searchResults.size > 0) {
- for (let i = 0; i < length; i++) {
- frame = this.frames[i];
- const vertexOffset = i * VERTICES;
- this.gl.uniform1i(
- this.uniforms.u_is_search_result,
- searchResults.has(getFlamegraphFrameSearchId(frame)) ? 1 : 0
- );
- this.gl.drawArrays(this.gl.TRIANGLES, vertexOffset, VERTICES);
- }
- } else {
- this.gl.uniform1i(this.uniforms.u_is_search_result, 0);
- for (let i = 0; i < length; i++) {
- const vertexOffset = i * VERTICES;
- this.gl.drawArrays(this.gl.TRIANGLES, vertexOffset, VERTICES);
- }
- }
- }
- }
- export {FlamegraphRenderer};
|