Skip to content

Commit

Permalink
Add flamegraph
Browse files Browse the repository at this point in the history
  • Loading branch information
Denis Bardadym committed Dec 3, 2023
1 parent f9a8724 commit 6eb3d19
Show file tree
Hide file tree
Showing 18 changed files with 73,751 additions and 36,430 deletions.
1 change: 1 addition & 0 deletions plugin/render-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const TEMPLATE_TYPE_RENDERED: Record<
treemap: buildHtml("treemap"),
"raw-data": async ({ data }) => outputRawData(data),
list: async ({ data }) => outputPlainTextList(data),
flamegraph: buildHtml("flamegraph"),
};

export const renderTemplate = (templateType: TemplateType, options: RenderTemplateOptions) => {
Expand Down
3 changes: 2 additions & 1 deletion plugin/template-types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"use strict";

export type TemplateType = "sunburst" | "treemap" | "network" | "raw-data" | "list";
export type TemplateType = "sunburst" | "treemap" | "network" | "raw-data" | "list" | "flamegraph";

const templates: ReadonlyArray<TemplateType> = [
"sunburst",
"treemap",
"network",
"list",
"raw-data",
"flamegraph",
];

export default templates;
2 changes: 1 addition & 1 deletion rollup.config-dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const postcssUrl = require("postcss-url");

const { visualizer } = require(".");

const HTML_TEMPLATE = ["treemap", "sunburst", "network"];
const HTML_TEMPLATE = ["treemap", "sunburst", "network", "flamegraph"];
const PLAIN_TEMPLATE = ["raw-data", "list"];
const ALL_TEMPLATE = [...HTML_TEMPLATE, ...PLAIN_TEMPLATE];

Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const alias = require("@rollup/plugin-alias");
const postcss = require("rollup-plugin-postcss");
const postcssUrl = require("postcss-url");

const HTML_TEMPLATE = ["treemap", "sunburst", "network"];
const HTML_TEMPLATE = ["treemap", "sunburst", "network", "flamegraph"];

/** @type {import('rollup').RollupOptions} */
module.exports = HTML_TEMPLATE.map((templateType) => ({
Expand Down
56 changes: 56 additions & 0 deletions src/flamegraph/chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { FunctionalComponent } from "preact";
import { useState, useEffect } from "preact/hooks";
import { HierarchyRectangularNode } from "d3-hierarchy";

import { ModuleTree, ModuleTreeLeaf, SizeKey } from "../../shared/types";
import { FlameGraph } from "./flamegraph";
import { Tooltip } from "./tooltip";

export interface ChartProps {
root: HierarchyRectangularNode<ModuleTree | ModuleTreeLeaf>;
sizeProperty: SizeKey;
selectedNode: HierarchyRectangularNode<ModuleTree | ModuleTreeLeaf> | undefined;
setSelectedNode: (
node: HierarchyRectangularNode<ModuleTree | ModuleTreeLeaf> | undefined
) => void;
}

export const Chart: FunctionalComponent<ChartProps> = ({
root,
sizeProperty,
selectedNode,
setSelectedNode,
}) => {
const [showTooltip, setShowTooltip] = useState<boolean>(false);
const [tooltipNode, setTooltipNode] = useState<
HierarchyRectangularNode<ModuleTree | ModuleTreeLeaf> | undefined
>(undefined);

useEffect(() => {
const handleMouseOut = () => {
setShowTooltip(false);
};

document.addEventListener("mouseover", handleMouseOut);
return () => {
document.removeEventListener("mouseover", handleMouseOut);
};
}, []);

return (
<>
<FlameGraph
root={root}
onNodeHover={(node) => {
setTooltipNode(node);
setShowTooltip(true);
}}
selectedNode={selectedNode}
onNodeClick={(node) => {
setSelectedNode(selectedNode === node ? undefined : node);
}}
/>
<Tooltip visible={showTooltip} node={tooltipNode} root={root} sizeProperty={sizeProperty} />
</>
);
};
81 changes: 81 additions & 0 deletions src/flamegraph/color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { scaleSequential, scaleLinear } from "d3-scale";
import { hsl, RGBColor } from "d3-color";

import { HierarchyNode } from "d3-hierarchy";
import { COLOR_BASE, CssColor } from "../color";
import { ModuleTree, ModuleTreeLeaf } from "../../shared/types";

// https://www.w3.org/TR/WCAG20/#relativeluminancedef
const rc = 0.2126;
const gc = 0.7152;
const bc = 0.0722;
// low-gamma adjust coefficient
const lowc = 1 / 12.92;

function adjustGamma(p: number) {
return Math.pow((p + 0.055) / 1.055, 2.4);
}

function relativeLuminance(o: RGBColor) {
const rsrgb = o.r / 255;
const gsrgb = o.g / 255;
const bsrgb = o.b / 255;

const r = rsrgb <= 0.03928 ? rsrgb * lowc : adjustGamma(rsrgb);
const g = gsrgb <= 0.03928 ? gsrgb * lowc : adjustGamma(gsrgb);
const b = bsrgb <= 0.03928 ? bsrgb * lowc : adjustGamma(bsrgb);

return r * rc + g * gc + b * bc;
}

export interface NodeColor {
backgroundColor: CssColor;
fontColor: CssColor;
}

export type NodeColorGetter = (node: HierarchyNode<ModuleTree | ModuleTreeLeaf>) => NodeColor;

const createRainbowColor = (root: HierarchyNode<ModuleTree | ModuleTreeLeaf>): NodeColorGetter => {
const colorParentMap = new Map<HierarchyNode<ModuleTree | ModuleTreeLeaf>, CssColor>();
colorParentMap.set(root, COLOR_BASE);

if (root.children != null) {
const colorScale = scaleSequential([0, root.children.length], (n) => hsl(360 * n, 0.3, 0.85));
root.children.forEach((c, id) => {
colorParentMap.set(c, colorScale(id).toString());
});
}

const colorMap = new Map<HierarchyNode<ModuleTree | ModuleTreeLeaf>, NodeColor>();

const lightScale = scaleLinear().domain([0, root.height]).range([0.9, 0.3]);

const getBackgroundColor = (node: HierarchyNode<ModuleTree | ModuleTreeLeaf>) => {
const parents = node.ancestors();
const colorStr =
parents.length === 1
? colorParentMap.get(parents[0])
: colorParentMap.get(parents[parents.length - 2]);

const hslColor = hsl(colorStr as string);
hslColor.l = lightScale(node.depth);

return hslColor;
};

return (node: HierarchyNode<ModuleTree | ModuleTreeLeaf>): NodeColor => {
if (!colorMap.has(node)) {
const backgroundColor = getBackgroundColor(node);
const l = relativeLuminance(backgroundColor.rgb());
const fontColor = l > 0.19 ? "#000" : "#fff";
colorMap.set(node, {
backgroundColor: backgroundColor.toString(),
fontColor,
});
}

return colorMap.get(node) as NodeColor;
};
};

export default createRainbowColor;
2 changes: 2 additions & 0 deletions src/flamegraph/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const TOP_PADDING = 20;
export const PADDING = 2;
42 changes: 42 additions & 0 deletions src/flamegraph/flamegraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { FunctionalComponent } from "preact";
import { useContext, useMemo } from "preact/hooks";
import { group } from "d3-array";
import { HierarchyNode, HierarchyRectangularNode } from "d3-hierarchy";

import { ModuleTree, ModuleTreeLeaf } from "../../shared/types";
import { Node } from "./node";
import { StaticContext } from "./index";

export interface FlameGraphProps {
root: HierarchyRectangularNode<ModuleTree | ModuleTreeLeaf>;
onNodeHover: (event: HierarchyRectangularNode<ModuleTree | ModuleTreeLeaf>) => void;
selectedNode: HierarchyRectangularNode<ModuleTree | ModuleTreeLeaf> | undefined;
onNodeClick: (node: HierarchyRectangularNode<ModuleTree | ModuleTreeLeaf>) => void;
}

export const FlameGraph: FunctionalComponent<FlameGraphProps> = ({
root,
onNodeHover,
selectedNode,
onNodeClick,
}) => {
const { width, height, getModuleIds } = useContext(StaticContext);



return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${width} ${height}`}>
{root.descendants().map((node) => {
return (
<Node
key={getModuleIds(node.data).nodeUid.id}
node={node}
onMouseOver={onNodeHover}
selected={selectedNode === node}
onClick={onNodeClick}
/>
);
})}
</svg>
);
};
129 changes: 129 additions & 0 deletions src/flamegraph/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { createContext, render } from "preact";
import {
hierarchy,
HierarchyNode,
partition,
PartitionLayout,
} from "d3-hierarchy";
import {
isModuleTree,
ModuleLengths,
ModuleTree,
ModuleTreeLeaf,
SizeKey,
VisualizerData,
} from "../../shared/types";

import { generateUniqueId, Id } from "../uid";
import { getAvailableSizeOptions } from "../sizes";
import { Main } from "./main";
import createRainbowColor, { NodeColorGetter } from "./color";

import "../style/style-flamegraph.scss";
import { PADDING } from "./const";

export interface StaticData {
data: VisualizerData;
availableSizeProperties: SizeKey[];
width: number;
height: number;
}

export interface ModuleIds {
nodeUid: Id;
clipUid: Id;
}

export interface ChartData {
layout: PartitionLayout<ModuleTree | ModuleTreeLeaf>;
rawHierarchy: HierarchyNode<ModuleTree | ModuleTreeLeaf>;
getModuleSize: (node: ModuleTree | ModuleTreeLeaf, sizeKey: SizeKey) => number;
getModuleIds: (node: ModuleTree | ModuleTreeLeaf) => ModuleIds;
getModuleColor: NodeColorGetter;
}

export type Context = StaticData & ChartData;

export const StaticContext = createContext<Context>({} as unknown as Context);

const drawChart = (
parentNode: Element,
data: VisualizerData,
width: number,
height: number,
): void => {
const availableSizeProperties = getAvailableSizeOptions(data.options);

console.time("layout create");

const layout = partition<ModuleTree | ModuleTreeLeaf>()
.size([width, height])
.padding(PADDING)
.round(true)

console.timeEnd("layout create");

console.time("rawHierarchy create");
const rawHierarchy = hierarchy<ModuleTree | ModuleTreeLeaf>(data.tree);
console.timeEnd("rawHierarchy create");

const nodeSizesCache = new Map<ModuleTree | ModuleTreeLeaf, ModuleLengths>();

const nodeIdsCache = new Map<ModuleTree | ModuleTreeLeaf, ModuleIds>();

const getModuleSize = (node: ModuleTree | ModuleTreeLeaf, sizeKey: SizeKey) =>
nodeSizesCache.get(node)?.[sizeKey] ?? 0;

console.time("rawHierarchy eachAfter cache");
rawHierarchy.eachAfter((node) => {
const nodeData = node.data;

nodeIdsCache.set(nodeData, {
nodeUid: generateUniqueId("node"),
clipUid: generateUniqueId("clip"),
});

const sizes: ModuleLengths = { renderedLength: 0, gzipLength: 0, brotliLength: 0 };
if (isModuleTree(nodeData)) {
for (const sizeKey of availableSizeProperties) {
sizes[sizeKey] = nodeData.children.reduce(
(acc, child) => getModuleSize(child, sizeKey) + acc,
0,
);
}
} else {
for (const sizeKey of availableSizeProperties) {
sizes[sizeKey] = data.nodeParts[nodeData.uid][sizeKey] ?? 0;
}
}
nodeSizesCache.set(nodeData, sizes);
});
console.timeEnd("rawHierarchy eachAfter cache");

const getModuleIds = (node: ModuleTree | ModuleTreeLeaf) => nodeIdsCache.get(node) as ModuleIds;

console.time("color");
const getModuleColor = createRainbowColor(rawHierarchy);
console.timeEnd("color");

render(
<StaticContext.Provider
value={{
data,
availableSizeProperties,
width,
height,
getModuleSize,
getModuleIds,
getModuleColor,
rawHierarchy,
layout,
}}
>
<Main />
</StaticContext.Provider>,
parentNode,
);
};

export default drawChart;
Loading

0 comments on commit 6eb3d19

Please sign in to comment.