Skip to content

Commit 8e5a178

Browse files
authored
Merge pull request #10 from TypeScriptToLua/feature/benchmark-visualizer
Feature/benchmark visualizer
2 parents b921432 + 82579fc commit 8e5a178

10 files changed

Lines changed: 1309 additions & 444 deletions

File tree

docusaurus.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ module.exports = {
3939
customCss: require.resolve("./src/custom.scss"),
4040
},
4141
pages: {
42-
include: ["index.tsx", "play/index.tsx"],
42+
include: ["index.tsx", "play/index.tsx", "benchviz/index.tsx"],
4343
},
4444
},
4545
],

package-lock.json

Lines changed: 956 additions & 442 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44
"start": "docusaurus start",
55
"build": "docusaurus build",
66
"lint": "npm run lint:prettier",
7-
"lint:prettier": "prettier --check ."
7+
"lint:prettier": "prettier --check .",
8+
"fix:prettier": "prettier --write ."
89
},
910
"prettier": {
1011
"printWidth": 120,
1112
"trailingComma": "all"
1213
},
1314
"dependencies": {
15+
"@types/d3": "^5.7.2",
1416
"@types/lz-string": "^1.3.34",
1517
"@types/react": "^16.9.35",
1618
"@types/react-dom": "^16.9.8",
1719
"@types/react-json-tree": "^0.6.11",
20+
"d3": "^5.16.0",
1821
"@types/webpack-env": "^1.15.2",
1922
"clsx": "^1.1.1",
2023
"fengari-web": "^0.1.4",

src/pages/benchviz/Benchmark.tsx

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import * as d3 from "d3";
2+
import React, { useEffect, useRef } from "react";
3+
import * as zlib from "zlib";
4+
import { BenchmarkResult, MemoryBenchmarkCategory } from "./benchmark-types";
5+
import { joinOnProperty, JoinResult } from "./util";
6+
import { barComparisonGraph } from "./visualizations/bar-comparison-graph";
7+
import { positiveNegativeBarGraph } from "./visualizations/positive-negative-bar-graph";
8+
9+
const garbageCreatedComparisonGraphWidth = 1000;
10+
const garbageCreatedComparisonGraphHeight = 300;
11+
12+
const garbageCreatedChangeGraphWidth = 1000;
13+
const garbageCreatedChangeGraphHeight = 300;
14+
15+
// Utility functions
16+
const formatBenchmarkName = (benchmarkName: string) => benchmarkName.replace(".lua", "").split("/").pop()!;
17+
const formatMemory = (value: number) => `${Math.round(value / 10) / 100} Mb`;
18+
19+
const benchmarkGarbage = (bm: BenchmarkResult) => bm.categories[MemoryBenchmarkCategory.Garbage];
20+
const garbagePercentChange = (result: JoinResult<BenchmarkResult>) =>
21+
(benchmarkGarbage(result.right!) - benchmarkGarbage(result.left!)) / benchmarkGarbage(result.left!);
22+
23+
export default function Benchmark() {
24+
let garbageCreatedChangeSvgRef = useRef<SVGSVGElement>(null!);
25+
let garbageCreatedComparisonSvgRef = useRef<SVGSVGElement>(null!);
26+
27+
const benchmarkData = decodeBenchmarkData(window.location.search.split("?d=")[1]);
28+
// Sort by percentage change of garbage created
29+
const benchmarksSortedByPercentDifference = benchmarkData.sort(
30+
(a, b) => garbagePercentChange(a) - garbagePercentChange(b),
31+
);
32+
33+
// Populate graph with benchmark results
34+
const benchmarkResultsTable = benchmarksSortedByPercentDifference.map((bm, i) => {
35+
const change = garbagePercentChange(bm);
36+
const rowColor = change === 0 ? "currentColor" : change > 0 ? "red" : "green";
37+
38+
return (
39+
<tr key={i} style={{ color: rowColor }}>
40+
<td>{bm.left!.benchmarkName}</td>
41+
<td>{formatMemory(benchmarkGarbage(bm.left!))}</td>
42+
<td>{formatMemory(benchmarkGarbage(bm.right!))}</td>
43+
<td>{change}</td>
44+
</tr>
45+
);
46+
});
47+
48+
// Comparison data master garbage created vs commit garbate created (PERCENTAGE CHANGE)
49+
const generatedGarbageChangeData = benchmarksSortedByPercentDifference.map((bm) => {
50+
const oldValue = bm.left!.categories[MemoryBenchmarkCategory.Garbage]!;
51+
const newValue = bm.right!.categories[MemoryBenchmarkCategory.Garbage]!;
52+
53+
return {
54+
name: formatBenchmarkName(bm.left!.benchmarkName || bm.right!.benchmarkName!),
55+
value: (100 * (newValue - oldValue)) / oldValue,
56+
};
57+
});
58+
59+
// Comparison data master garbage created vs commit garbate created (ABSOLUTE)
60+
const generatedGarbageData = benchmarksSortedByPercentDifference.map((bm) => ({
61+
name: formatBenchmarkName(bm.left!.benchmarkName || bm.right!.benchmarkName!),
62+
oldValue: bm.left!.categories[MemoryBenchmarkCategory.Garbage] || 0,
63+
newValue: bm.right!.categories[MemoryBenchmarkCategory.Garbage] || 0,
64+
}));
65+
66+
useEffect(() => {
67+
// Populate graph showing percentual change in garbage created
68+
positiveNegativeBarGraph(
69+
d3.select(garbageCreatedChangeSvgRef.current),
70+
generatedGarbageChangeData,
71+
garbageCreatedChangeGraphWidth,
72+
garbageCreatedChangeGraphHeight,
73+
);
74+
75+
// Populate graph showing absolute garbage created numbers
76+
barComparisonGraph(
77+
d3.select(garbageCreatedComparisonSvgRef.current),
78+
generatedGarbageData,
79+
garbageCreatedComparisonGraphWidth,
80+
garbageCreatedComparisonGraphHeight,
81+
);
82+
});
83+
84+
return (
85+
<>
86+
<h2>Benchmark results</h2>
87+
{/* Results table */}
88+
<table>
89+
<thead>
90+
<tr style={{ fontWeight: "bold" }}>
91+
<td>Benchmark</td>
92+
<td>Garbage Master</td>
93+
<td>Garbage Commit</td>
94+
<td>% Change</td>
95+
</tr>
96+
</thead>
97+
<tbody>{benchmarkResultsTable}</tbody>
98+
</table>
99+
100+
<h2>Garbage created change</h2>
101+
{/* [% Delta] Gerbage created */}
102+
<svg
103+
ref={garbageCreatedChangeSvgRef}
104+
width={garbageCreatedChangeGraphWidth}
105+
height={garbageCreatedChangeGraphHeight}
106+
></svg>
107+
108+
<h2>Garbage created</h2>
109+
{/* [Absolute] Garbage created comparison */}
110+
<svg
111+
ref={garbageCreatedComparisonSvgRef}
112+
width={garbageCreatedComparisonGraphWidth}
113+
height={garbageCreatedComparisonGraphHeight}
114+
></svg>
115+
</>
116+
);
117+
}
118+
119+
function decodeBenchmarkData(encodedData: string) {
120+
const results = JSON.parse(zlib.inflateSync(Buffer.from(encodedData, "base64")).toString());
121+
122+
const dataMaster = results.old as BenchmarkResult[];
123+
const dataCommit = results.new as BenchmarkResult[];
124+
125+
// Match old/new results by name
126+
return joinOnProperty(dataMaster, dataCommit, (bm) => bm.benchmarkName);
127+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export enum BenchmarkKind {
2+
Memory = "memory",
3+
}
4+
5+
export type BenchmarkResult = MemoryBenchmarkResult;
6+
7+
export enum MemoryBenchmarkCategory {
8+
TotalMemory = "totalMemory",
9+
Garbage = "garbage",
10+
}
11+
12+
export interface MemoryBenchmarkResult {
13+
kind: string;
14+
categories: Record<MemoryBenchmarkCategory, number>;
15+
benchmarkName: string;
16+
}

src/pages/benchviz/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Layout from "@theme/Layout";
2+
import React from "react";
3+
import Benchmark from "./Benchmark";
4+
5+
export default function CreateBenchmark() {
6+
const isSSR = typeof window === "undefined";
7+
return <Layout title="Benchmark">{!isSSR && <Benchmark />}</Layout>;
8+
}

src/pages/benchviz/util.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export interface JoinResult<T> {
2+
left?: T;
3+
right?: T;
4+
}
5+
6+
export function joinOnProperty<TItem, TKey>(
7+
left: TItem[],
8+
right: TItem[],
9+
propertySelector: (item: TItem) => TKey,
10+
): Array<JoinResult<TItem>> {
11+
const map = new Map<TKey, JoinResult<TItem>>();
12+
13+
for (const item of left) {
14+
map.set(propertySelector(item), { left: item });
15+
}
16+
for (const item of right) {
17+
const key = propertySelector(item);
18+
const entry = map.get(key);
19+
if (entry) {
20+
entry.right = item;
21+
} else {
22+
map.set(key, { right: item });
23+
}
24+
}
25+
26+
return [...map.values()];
27+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import * as d3 from "d3";
2+
import { addLegend } from "./d3-util";
3+
4+
interface ComparisonData {
5+
name: string;
6+
oldValue: number;
7+
newValue: number;
8+
}
9+
10+
const GRAPH_MARGIN = { left: 50, top: 5, right: 1 };
11+
12+
const COLOR_OLD = "#888888";
13+
const COLOR_NEW = "#007ACC";
14+
15+
export function barComparisonGraph(
16+
selection: d3.Selection<SVGSVGElement, unknown, null, undefined>,
17+
data: ComparisonData[],
18+
width: number,
19+
height: number,
20+
) {
21+
const barMaxHeight = height - 50;
22+
23+
// Create X scale and axis
24+
const xScale = d3
25+
.scaleBand()
26+
.domain(data.map((bm) => bm.name))
27+
.range([GRAPH_MARGIN.left, width - GRAPH_MARGIN.right]);
28+
29+
const bandWidth = xScale.bandwidth();
30+
const barWidth = 25;
31+
32+
const xAxis = d3.axisBottom(xScale);
33+
selection.append("g").attr("transform", `translate(0, ${barMaxHeight})`).call(xAxis);
34+
35+
// Create Y scale and axis
36+
const maxValue = d3.max(data.map((bm) => Math.max(bm.oldValue, bm.newValue)))!;
37+
const maxAxisValue = Math.pow(10, Math.ceil(Math.log10(maxValue)));
38+
39+
const yScale = d3
40+
.scaleLog()
41+
.domain([1, maxAxisValue])
42+
.range([barMaxHeight - GRAPH_MARGIN.top, 0]);
43+
44+
const yAxis = d3.axisLeft(yScale);
45+
selection.append("g").attr("transform", `translate(${GRAPH_MARGIN.left}, ${GRAPH_MARGIN.top})`).call(yAxis);
46+
47+
// Create bars for each entry
48+
const entries = selection.selectAll("rect").data(data).enter();
49+
50+
entries
51+
.append("rect")
52+
.attr("width", barWidth)
53+
.attr("x", (d) => xScale(d.name)! + 0.5 * bandWidth - barWidth - 1)
54+
.attr("height", (d) => barMaxHeight - yScale(d.oldValue))
55+
.attr("y", (d) => yScale(d.oldValue))
56+
.style("fill", COLOR_OLD);
57+
//.style("stroke", "currentColor");
58+
59+
entries
60+
.append("rect")
61+
.attr("width", barWidth)
62+
.attr("x", (d) => xScale(d.name)! + 0.5 * bandWidth + 1)
63+
.attr("height", (d) => barMaxHeight - yScale(d.newValue))
64+
.attr("y", (d) => yScale(d.newValue))
65+
.style("fill", COLOR_NEW);
66+
//.style("stroke", "currentColor");
67+
68+
// Add legend
69+
const legendEntries: Array<[string, string]> = [
70+
["Master", COLOR_OLD],
71+
["Commit", COLOR_NEW],
72+
];
73+
addLegend(selection, legendEntries).attr(
74+
"transform",
75+
`translate(${width - 100 * legendEntries.length - GRAPH_MARGIN.right}, ${height - 20})`,
76+
);
77+
78+
return selection;
79+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as d3 from "d3";
2+
3+
export function addLegend(
4+
selection: d3.Selection<SVGSVGElement, unknown, null, undefined>,
5+
items: Array<[string, string]>,
6+
) {
7+
const legend = selection.append("g");
8+
9+
for (const [index, [name, color]] of items.entries()) {
10+
legend
11+
.append("rect")
12+
.attr("width", 15)
13+
.attr("height", 15)
14+
.attr("x", 100 * index)
15+
.style("fill", color)
16+
.style("stroke", "currentColor");
17+
18+
legend
19+
.append("text")
20+
.attr("x", 100 * index + 20)
21+
.attr("y", 13)
22+
.text(name)
23+
.attr("fill", "currentColor");
24+
}
25+
26+
return legend;
27+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as d3 from "d3";
2+
3+
interface CategoryData {
4+
name: string;
5+
value: number;
6+
}
7+
8+
const GRAPH_MARGIN = { left: 50, top: 5, right: 1 };
9+
10+
export function positiveNegativeBarGraph(
11+
selection: d3.Selection<SVGSVGElement, unknown, null, undefined>,
12+
data: CategoryData[],
13+
width: number,
14+
height: number,
15+
) {
16+
const minValue = d3.min(data.map((bm) => bm.value))!;
17+
const maxValue = d3.max(data.map((bm) => bm.value))!;
18+
19+
const yScale = d3
20+
.scaleLinear()
21+
.domain([minValue * 1.2, maxValue * 1.2])
22+
.range([0, height - 10]);
23+
24+
const yAxis = d3.axisLeft(yScale);
25+
26+
const xScale = d3
27+
.scaleBand()
28+
.domain(data.map((bm) => bm.name))
29+
.range([GRAPH_MARGIN.left, width - GRAPH_MARGIN.right]);
30+
31+
const bandWidth = xScale.bandwidth();
32+
const barWidth = 25;
33+
34+
const xAxis = d3.axisBottom(xScale);
35+
36+
selection
37+
.append("g")
38+
.attr("transform", `translate(0, ${yScale(0) + 5})`)
39+
.call(xAxis);
40+
41+
selection.append("g").attr("transform", `translate(${GRAPH_MARGIN.left}, ${GRAPH_MARGIN.top})`).call(yAxis);
42+
43+
const barSize = (val: number) => Math.abs(height / 2 - yScale(val));
44+
45+
const bars = selection.selectAll("rect").data(data).enter();
46+
47+
bars.append("rect")
48+
.attr("width", barWidth)
49+
.attr("x", (d) => xScale(d.name)! + 0.5 * bandWidth - 0.5 * barWidth)
50+
.attr("height", (d) => barSize(d.value) - 1)
51+
.attr("y", (d) => (d.value > 0 ? height / 2 + 10 : height / 2 + 10 - barSize(d.value)))
52+
.style("fill", (d) => (d.value > 0 ? "red" : "green"));
53+
//.style("stroke", "currentColor");
54+
55+
bars.append("text")
56+
.text((d) => `${d.value > 0 ? "+" : ""}${Math.round(d.value * 100) / 100}%`)
57+
.attr("x", (d) => xScale(d.name)! + 0.5 * bandWidth)
58+
.attr("y", (d) => (d.value > 0 ? height / 2 + barSize(d.value) + 30 : height / 2 - barSize(d.value)))
59+
.style("fill", "currentColor")
60+
.style("text-anchor", "middle");
61+
//.style("stroke", "currentColor");
62+
63+
return selection;
64+
}

0 commit comments

Comments
 (0)