blob: 6bf2b7fc850502356d7caccd3a2138c4f210957a [file] [log] [blame]
// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {CallsiteInfo} from './globals';
interface Node {
width: number;
startX: number;
size: number;
}
const NODE_HEIGHT = 10;
export class Flamegraph {
private nodeHeight = NODE_HEIGHT;
private flamegraphData: CallsiteInfo[];
constructor(flamegraphData: CallsiteInfo[]) {
this.flamegraphData = flamegraphData;
}
hash(s: string): number {
let hash = 0x811c9dc5 & 0xfffffff;
for (let i = 0; i < s.length; i++) {
hash ^= s.charCodeAt(i);
hash = (hash * 16777619) & 0xffffffff;
}
return hash & 0xff;
}
generateColor(name: string|undefined): string {
if (name === undefined) {
return 'grey';
}
const hue = this.hash(name);
const saturation = 50;
return `hsl(${hue}, ${saturation}%, 65%)`;
}
/**
* Caller will have to call draw method ater updating data to have updated
* graph.
*/
updateData(flamegraphData: CallsiteInfo[]) {
this.flamegraphData = flamegraphData;
}
draw(
ctx: CanvasRenderingContext2D, width: number, height: number, x = 0,
y = 0, nodeHeight: number = NODE_HEIGHT) {
this.nodeHeight = nodeHeight;
if (this.flamegraphData !== undefined) {
// Finding total size of roots.
let totalSize = 0;
this.flamegraphData.forEach((value: CallsiteInfo) => {
if (value.parentHash === -1) {
totalSize += value.totalSize;
}
});
// For each node, we use this map to get information about it's parent
// (total size of it, width and where it starts in graph) so we can
// calculate it's own position in graph.
const node = new Map<number, Node>();
let currentY = y;
node.set(-1, {width, startX: x, size: totalSize});
// Draw root node.
ctx.fillStyle = 'grey';
ctx.fillRect(x, currentY, width, this.nodeHeight);
currentY += this.nodeHeight;
for (let i = 0; i < this.flamegraphData.length; i++) {
if (currentY > height) {
break;
}
const value = this.flamegraphData[i];
const parentNode = node.get(value.parentHash);
if (parentNode !== undefined) {
const parent = value.parentHash;
const parentSize = parent === -1 ? totalSize : parentNode.size;
// Calculate node's width based on its proportion in parent.
const width = value.totalSize / parentSize * parentNode.width;
// Draw node.
const currentX = parentNode.startX;
currentY = this.nodeHeight * (value.depth + 1);
ctx.fillStyle = this.generateColor(value.name);
ctx.fillRect(currentX, currentY, width, this.nodeHeight);
// Draw border around node.
ctx.strokeStyle = 'white';
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX, currentY + this.nodeHeight);
ctx.lineTo(currentX + width, currentY + this.nodeHeight);
ctx.lineTo(currentX + width, currentY);
ctx.moveTo(currentX, currentY);
ctx.lineWidth = 1;
ctx.closePath();
ctx.stroke();
// Set current node's data in map for children to use.
node.set(
value.hash, {width, startX: currentX, size: value.totalSize});
// Update next x coordinate in parent.
node.set(value.parentHash, {
width: parentNode.width,
startX: currentX + width,
size: parentNode.size
});
}
}
}
}
}