Merge branch 'treemap'

This commit is contained in:
Tom McLaughlin 2024-02-09 17:36:51 -08:00
commit 02e62d08d9
12 changed files with 5360 additions and 11812 deletions

13
CHANGELOG.md Normal file
View File

@ -0,0 +1,13 @@
# ChangeLog
## 2.0.0
* Added TreeMap visualizations.
## 1.0.1
* Be able to control dependency paths with environment variables so it's easier to package on Nixpkgs.
## 1.0.0
* Initial release.

View File

@ -58,7 +58,7 @@ stack build --ghc-options "-ddump-to-file -ddump-timings"
# Tips
The script will output its log messages to `stderr` and print the final report path to `stdout` (assuming it didn't exit with a failure). This makes it easy to use the output in scripts. For example:
* The script will output its log messages to `stderr` and print the final report path to `stdout` (assuming it didn't exit with a failure). This makes it easy to use the output in scripts. For example:
``` shell
# Build the report and open it in your browser
@ -70,6 +70,18 @@ The script will output its log messages to `stderr` and print the final report p
> cp $(/path/to/time-ghc-modules/time-ghc-modules) $MY_CI_ARTIFACTS_DIR/
```
* You can also look at the timing of individual components, but doing e.g. `stack build some-component:lib`. But, make sure to clean up any old `.dump-timings` files from previous runs:
``` shell
find . -name "*.dump-timings" | xargs rm
```
* GHC's `-dumpdir` option can be used to consolidate the `.dump-timings` files, so they aren't left all over your source tree. For example:
``` shell
stack build --ghc-options "-ddump-to-file -ddump-timings -dumpdir .ghcdump"
```
# Compatibility
The flag `-ddump-timings` is available for `GHC >= 8.4.1`.

22
dist/index.html vendored

File diff suppressed because one or more lines are too long

14386
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,20 +20,35 @@
},
"homepage": "https://github.com/codedownio/time-ghc-modules#readme",
"devDependencies": {
"@types/d3-hierarchy": "^3.1.6",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-scale": "^4.0.8",
"@types/d3-scale-chromatic": "^3.0.3",
"@types/d3-selection": "^3.0.10",
"@types/lodash": "^4.14.171",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"process": "^0.11.10",
"rimraf": "^3.0.2"
},
"dependencies": {
"@codedown/react-d3-treemap": "^1.0.31",
"@material-ui/core": "^4.12.3",
"apexcharts": "^3.27.3",
"d3-hierarchy": "^3.1.2",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-scale-chromatic": "^3.0.0",
"d3-selection": "^3.0.0",
"lodash": "^4.17.21",
"parcel": "^2.0.0-beta.2",
"parcel": "^2.11.0",
"react": "^17.0.2",
"react-apexcharts": "^1.3.9",
"react-dom": "^17.0.2",
"tachyons": "^4.12.0",
"typescript": "^4.3.5"
},
"@parcel/transformer-css": {
"errorRecovery": true
}
}

27
src/TreeMap.css Normal file
View File

@ -0,0 +1,27 @@
.TreeMap__breadcrumb {
padding-bottom: 0.5em;
padding-top: 0.5em;
}
.TreeMap__breadcrumb > div {
display: flex;
align-items: center;
}
.TreeMap__breadcrumbItem {
font-size: 1em !important;
}
/* a.TreeMap__breadcrumbItem[style*="cursor:pointer"] { */
.TreeMap__breadcrumb div:not(:last-child) a.TreeMap__breadcrumbItem {
font-weight: bold;
}
.AppTreeMap__node {
rx: 0;
}
.AppTreeMap__svg {
font-family: "Courier New", Courier, monospace;
}

View File

@ -1,10 +1,15 @@
import {size} from "lodash";
import {Treemap as RVTreeMap} from "react-vis";
import { scaleSequential } from "d3-scale";
import * as chromatic from "d3-scale-chromatic";
import { debounce } from "lodash";
import { CSSProperties, useEffect, useMemo, useRef, useState} from "react";
import "react-vis/dist/style.css";
import buildNestedData, {removeEmptyNodes} from "./TreeMap/BuildData";
import {formatBytes, formatTime} from "./Util";
// import {formatBytes, formatTime} from "./Util";
// import SimpleD3TreeMap from "./D3TreeMap";
import D3TreeMap, { ColorModel, NumberOfChildrenPlacement } from "@codedown/react-d3-treemap";
interface Props {
@ -12,53 +17,88 @@ interface Props {
data: ModuleData;
}
const wrapperStyle: CSSProperties = {
width: "100%",
height: "80vh",
};
const paddingPx = 0;
const svgStyle: CSSProperties = {
marginLeft: "-" + paddingPx + "px",
}
export default function TreeMap({aggregate, data}: Props) {
const modulesList = aggregate === "time" ? data.modulesByTime : data.modulesByAlloc;
const numModules = size(modulesList);
console.log("modulesList", modulesList);
const nestedData: Tree<TreeNode> = useMemo(() => {
const tree = buildNestedData(aggregate, modulesList);
return removeEmptyNodes(tree);
}, [aggregate, modulesList]);
const myData = {
"title": "analytics",
"color": "#12939A",
"children": [
{
"title": "cluster",
"children": [
{"title": "AgglomerativeCluster", "color": "#12939A", "size": 3938},
{"title": "CommunityStructure", "color": "#12939A", "size": 3812},
{"title": "HierarchicalCluster", "color": "#12939A", "size": 6714},
{"title": "MergeEdge", "color": "#12939A", "size": 743}
]
},
{
"title": "graph",
"children": [
{"title": "BetweennessCentrality", "color": "#12939A", "size": 3534},
{"title": "LinkDistance", "color": "#12939A", "size": 5731},
{"title": "MaxFlowMinCut", "color": "#12939A", "size": 7840},
{"title": "ShortestPaths", "color": "#12939A", "size": 5914},
{"title": "SpanningTree", "color": "#12939A", "size": 3416}
]
},
{
"title": "optimization",
"children": [
{"title": "AspectRatioBanker", "color": "#12939A", "size": 7074}
]
}
]
}
const ref = useRef(null);
const [dimensions, setDimensions] = useState<{ height: number, width: number } | null>(null);
const elementObserver = useMemo(() => {
return new ResizeObserver(() => {
debounce(() => {
if (!ref.current) return;
setDimensions({
height: ref.current.clientHeight,
width: ref.current.clientWidth
});
}, 10)();
});
}, [ref.current]);
useEffect(() => {
if (!ref) return;
const element = ref.current;
// Modes: squarify, resquarify, slice, dice, slicedice, binary, circlePack, partition, partition-pivot
elementObserver.observe(element);
return () => {
elementObserver.unobserve(element);
};
}, [ref.current, elementObserver]);
return (
<RVTreeMap
title={'My New Treemap'}
width={300}
height={300}
data={myData}
mode="binary"
/>
<div ref={ref}
style={wrapperStyle}>
{dimensions &&
<D3TreeMap<Tree<TreeNode>>
key={aggregate}
id="myTreeMap"
width={dimensions.width + paddingPx}
svgStyle={svgStyle}
height={dimensions.height}
data={nestedData}
valueUnit=""
levelsToDisplay={2}
paddingInner={paddingPx}
paddingOuter={constZero}
paddingTop={constZero}
nodeClassName="AppTreeMap__node"
nodeStyle={{
fontSize: 12,
paddingTop: 2,
paddingLeft: 5,
paddingRight: 5,
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
}}
hideNumberOfChildren={true}
customD3ColorScale={scaleSequential(
chromatic.interpolateSpectral
)}
colorModel={ColorModel.OneEachChildren}
darkNodeBorderColor="silver"
darkNodeTextColor="white"
lightNodeBorderColor="brown"
lightNodeTextColor="brown"
valueFn={aggregate === "time" ? formatTime : formatBytes}
breadCrumbClassName="ba b--silver pa1 mv1"
/>
}
</div>
);
}
const constZero = () => 0;

69
src/TreeMap/BuildData.ts Normal file
View File

@ -0,0 +1,69 @@
import {find, forEach} from "lodash";
type ModuleItem = {module: string, alloc: number} | {module: string, time: number};
export default function buildNestedData(
aggregate: Aggregate,
modulesList: Array<ModuleItem>
) {
const ret: Tree<TreeNode> = {
name: "Top-level",
value: 0,
part: "",
children: [],
};
for (const m of modulesList) {
addModuleToTree(aggregate, m, ret);
}
forEach(ret.children, (x) => {
ret.value += x.value;
});
console.log("Built nestedData", ret);
return ret;
}
function addModuleToTree(aggregate: Aggregate, m: ModuleItem, ret: Tree<TreeNode>) {
let current = ret;
let soFar = "";
const parts = m.module.split(".");
for (let i = 0; i < parts.length; i += 1) {
const part = parts[i];
soFar += (i === 0 ? part : "." + part);
let item = find(current.children, (x) => x.part === part);
if (!item) {
item = {
name: soFar,
part,
value: 0,
children: [],
};
current.children.push(item);
}
current = item;
// Add the value to the bottommost node
if (i === parts.length - 1) {
current.value += (aggregate === "time" ? m["time"]: m["alloc"]);
}
}
}
export function removeEmptyNodes(tree: Tree<TreeNode>): Tree<TreeNode> {
for (let i = 0; i < tree.children.length; i += 1) {
tree.children[i] = removeEmptyNodes(tree.children[i]);
}
if (tree.value === 0 && tree.children.length === 1) {
return tree.children[0];
} else {
return tree;
}
}

View File

@ -11,14 +11,16 @@
</script>
<style>
@import "tachyons";
@import "../node_modules/tachyons/css/tachyons.css";
@import "../node_modules/@codedown/react-d3-treemap/dist/react.d3.treemap.css";
@import "./TreeMap.css";
</style>
</head>
<body>
<div id="root"></div>
<script>
<script type="module">
import "./index.tsx";
</script>
</body>

2475
src/index_test.html Normal file

File diff suppressed because it is too large Load Diff

12
src/types.d.ts vendored
View File

@ -17,3 +17,15 @@ interface ModuleData {
modules: Array<{module: string;}>;
data: Array<Event>;
}
type Tree<D> = D & {
children?: Tree<D>[];
id?: string;
}
interface TreeNode {
name: string;
part: string;
color?: string;
value: number;
}

View File

@ -1,5 +1,6 @@
{
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"moduleResolution": "node",
"noImplicitAny": false,
"removeComments": true,