Skip to content

Commit df4609a

Browse files
committed
perf: enhance performance while using with large binary
Signed-off-by: Zxilly <[email protected]>
1 parent 77251b6 commit df4609a

File tree

6 files changed

+1400
-2926
lines changed

6 files changed

+1400
-2926
lines changed

ui/src/Node.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,6 @@ export const Node: React.FC<NodeProps> = React.memo((
8383
textRef.current.setAttribute("transform", `scale(${scale.toFixed(2)})`);
8484
}, [hasChildren, height, width]);
8585

86-
if (width === 0 || height === 0) {
87-
return null;
88-
}
89-
9086
return (
9187
<g
9288
className="node"

ui/src/TreeMap.tsx

Lines changed: 135 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { useCallback, useEffect, useMemo, useState } from "react";
1+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
22
import { group } from "d3-array";
3-
import type { HierarchyNode, HierarchyRectangularNode } from "d3-hierarchy";
3+
import type { HierarchyNode } from "d3-hierarchy";
44
import { hierarchy, treemap, treemapSquarify } from "d3-hierarchy";
55
import { useTitle, useWindowSize } from "react-use";
66

@@ -11,6 +11,8 @@ import { Node } from "./Node.tsx";
1111

1212
import "./style.scss";
1313
import { trimPrefix } from "./tool/utils.ts";
14+
import { shallowCopy } from "./tool/copy.ts";
15+
import { useMouse } from "./tool/useMouse.ts";
1416

1517
interface TreeMapProps {
1618
entry: Entry;
@@ -26,17 +28,36 @@ function TreeMap({ entry }: TreeMapProps) {
2628
const { width, height } = useWindowSize();
2729

2830
const rawHierarchy = useMemo(() => {
29-
return hierarchy(entry, e => e.getChildren());
31+
return hierarchy(entry, e => e.getChildren())
32+
.sum((e) => {
33+
if (e.getChildren().length === 0) {
34+
return e.getSize();
35+
}
36+
return 0;
37+
})
38+
.sort((a, b) => a.data.getSize() - b.data.getSize());
3039
}, [entry]);
3140

32-
const getModuleColor = useMemo(() => {
41+
const rawHierarchyID = useMemo(() => {
42+
const cache = new Map<number, HierarchyNode<Entry>>();
43+
rawHierarchy.descendants().forEach((node) => {
44+
cache.set(node.data.getID(), node);
45+
});
46+
return cache;
47+
}, [rawHierarchy]);
48+
49+
const getModuleColorRaw = useMemo(() => {
3350
return createRainbowColor(rawHierarchy);
3451
}, [rawHierarchy]);
3552

53+
const getModuleColor = useCallback((id: number) => {
54+
return getModuleColorRaw(rawHierarchyID.get(id)!);
55+
}, [getModuleColorRaw, rawHierarchyID]);
56+
3657
const layout = useMemo(() => {
3758
return treemap<Entry>()
3859
.size([width, height])
39-
.paddingInner(2)
60+
.paddingInner(1)
4061
.paddingTop(20)
4162
.round(true)
4263
.tile(treemapSquarify);
@@ -64,66 +85,66 @@ function TreeMap({ entry }: TreeMapProps) {
6485
cur = found;
6586
}
6687

67-
return cur;
88+
return cur.data.getID();
6889
}, [entry, rawHierarchy]);
6990

70-
const [selectedNode, setSelectedNode] = useState<HierarchyNode<Entry> | null>(loadNodeFromHash);
71-
const selectedNodeLeaveIDSet = useMemo(() => {
72-
if (selectedNode === null) {
73-
return new Set<number>();
91+
const [selectedNodeID, setSelectedNodeID] = useState<number | null>(loadNodeFromHash);
92+
93+
const layoutRoot = useMemo(() => {
94+
let root: HierarchyNode<Entry> | null;
95+
if (!selectedNodeID || selectedNodeID === rawHierarchy.data.getID()) {
96+
root = rawHierarchy;
7497
}
98+
else {
99+
const selectedNode = rawHierarchyID.get(selectedNodeID)!;
100+
const ancestors = selectedNode.ancestors().reverse();
75101

76-
return new Set(selectedNode.leaves().map(d => d.data.getID()));
77-
}, [selectedNode]);
102+
function writeValue(n: HierarchyNode<Entry>, value?: number) {
103+
// @ts-expect-error write to readonly
104+
// noinspection JSConstantReassignment
105+
n.value = value;
106+
}
78107

79-
const root = useMemo(() => {
80-
const rootWithSizesAndSorted = rawHierarchy
81-
.sum((node) => {
82-
if (node.getChildren().length === 0) {
83-
if (selectedNode) {
84-
if (!selectedNodeLeaveIDSet.has(node.getID())) {
85-
return 0;
86-
}
87-
}
108+
root = shallowCopy(rawHierarchy);
109+
writeValue(root, selectedNode.value);
88110

89-
return node.getSize();
90-
}
91-
return 0;
92-
})
93-
.sort((a, b) => a.data.getSize() - b.data.getSize());
111+
let cur = root;
112+
for (let i = 1; i < ancestors.length; i++) {
113+
// use shallowCopy
114+
const node = shallowCopy(ancestors[i]);
115+
writeValue(node, selectedNode.value);
116+
cur.children = [node];
117+
cur = node;
118+
}
119+
}
94120

95-
return layout(rootWithSizesAndSorted);
96-
}, [layout, rawHierarchy, selectedNode, selectedNodeLeaveIDSet]);
121+
return layout(root!);
122+
}, [layout, rawHierarchy, rawHierarchyID, selectedNodeID]);
97123

98-
const nestedData = useMemo(() => {
99-
const nestedDataMap = group(
100-
root.descendants(),
124+
const layers = useMemo(() => {
125+
const layerMap = group(
126+
layoutRoot.descendants(),
101127
(d: HierarchyNode<Entry>) => d.height,
102128
);
103-
const nested = Array.from(nestedDataMap, ([key, values]) => ({
129+
const layerArray = Array.from(layerMap, ([key, values]) => ({
104130
key,
105131
values,
106132
}));
107-
nested.sort((a, b) => b.key - a.key);
108-
return nested;
109-
}, [root]);
110-
111-
const allNodes = useMemo(() => {
112-
const cache = new Map<number, HierarchyRectangularNode<Entry>>();
113-
root.descendants().forEach((node) => {
114-
cache.set(node.data.getID(), node);
115-
});
116-
return cache;
117-
}, [root]);
133+
layerArray.sort((a, b) => b.key - a.key);
134+
return layerArray;
135+
}, [layoutRoot]);
118136

119137
useEffect(() => {
120-
if (selectedNode === null) {
138+
if (selectedNodeID === null) {
121139
if (location.hash !== "") {
122140
history.replaceState(null, "", " ");
123141
}
124142
return;
125143
}
126-
144+
const selectedNode = rawHierarchyID.get(selectedNodeID);
145+
if (!selectedNode) {
146+
return;
147+
}
127148
const path = `#${selectedNode
128149
.ancestors()
129150
.map((d) => {
@@ -135,27 +156,21 @@ function TreeMap({ entry }: TreeMapProps) {
135156
if (location.hash !== path) {
136157
history.replaceState(null, "", path);
137158
}
138-
}, [selectedNode]);
159+
}, [rawHierarchyID, selectedNodeID]);
139160

140-
const [showTooltip, setShowTooltip] = useState(false);
141-
const [tooltipPosition, setTooltipPosition] = useState<[number, number]>([0, 0]);
142161
const [tooltipNode, setTooltipNode]
143-
= useState<HierarchyRectangularNode<Entry> | undefined>(undefined);
144-
145-
const onMouseEnter = useCallback(() => {
146-
setShowTooltip(true);
147-
}, []);
148-
149-
const onMouseLeave = useCallback(() => {
150-
setShowTooltip(false);
151-
}, []);
152-
153-
const getTargetNode = useCallback((e: React.MouseEvent<SVGSVGElement>) => {
154-
if (!e.target) {
155-
return null;
156-
}
157-
158-
const target = (e.target as SVGElement).parentNode;
162+
= useState<Entry | undefined>(undefined);
163+
const svgRef = useRef<SVGSVGElement>(null);
164+
165+
const {
166+
clientX,
167+
clientY,
168+
isOver,
169+
eventTarget: mouseEventTarget,
170+
} = useMouse(svgRef);
171+
172+
const getTargetNode = useCallback((e: EventTarget) => {
173+
const target = (e as SVGElement).parentNode;
159174
if (!target) {
160175
return null;
161176
}
@@ -167,85 +182,83 @@ function TreeMap({ entry }: TreeMapProps) {
167182

168183
const dataId = Number.parseInt(dataIdStr);
169184

170-
return allNodes.get(dataId) ?? null;
171-
}, [allNodes]);
172-
173-
const onMouseMove = useCallback((e: React.MouseEvent<SVGSVGElement>) => {
174-
setTooltipPosition([e.clientX, e.clientY]);
185+
return rawHierarchyID.get(dataId)?.data ?? null;
186+
}, [rawHierarchyID]);
175187

176-
const node = getTargetNode(e);
188+
const onClick = useCallback((e: React.MouseEvent<SVGSVGElement>) => {
189+
const node = getTargetNode(e.target);
177190
if (node === null) {
178-
setTooltipNode(undefined);
179191
return;
180192
}
181-
setTooltipNode(node);
182-
}, [getTargetNode]);
183193

184-
const onClick = useCallback((e: React.MouseEvent<SVGSVGElement>) => {
185-
const node = getTargetNode(e);
186-
if (node === null) {
194+
if (e.ctrlKey) {
195+
console.log(node);
187196
return;
188197
}
189198

190-
if (selectedNode?.data.getID() === node.data.getID()) {
191-
setSelectedNode(null);
199+
if (selectedNodeID === node.getID()) {
200+
setSelectedNodeID(null);
192201
}
193202
else {
194-
setSelectedNode(node);
203+
setSelectedNodeID(node.getID());
195204
}
196-
}, [getTargetNode, selectedNode]);
205+
}, [getTargetNode, selectedNodeID]);
206+
207+
const tooltipVisible = useMemo(() => {
208+
return isOver && tooltipNode;
209+
}, [isOver, tooltipNode]);
210+
211+
useEffect(() => {
212+
if (!mouseEventTarget) {
213+
return;
214+
}
215+
const node = getTargetNode(mouseEventTarget);
216+
if (node) {
217+
setTooltipNode(node);
218+
}
219+
}, [getTargetNode, mouseEventTarget]);
197220

198221
const nodes = useMemo(() => {
199-
const selectedID = selectedNode?.data.getID();
200-
201-
return (
202-
nestedData.map(({ key, values }) => {
203-
return (
204-
<g className="layer" key={key}>
205-
{values.map((node) => {
206-
const { backgroundColor, fontColor } = getModuleColor(node);
207-
208-
if (node.x0 === node.x1 || node.y0 === node.y1) {
209-
return null;
210-
}
211-
212-
return (
213-
<Node
214-
key={node.data.getID()}
215-
id={node.data.getID()}
216-
title={node.data.getName()}
217-
selected={selectedID === node.data.getID()}
218-
x0={node.x0}
219-
y0={node.y0}
220-
x1={node.x1}
221-
y1={node.y1}
222-
223-
backgroundColor={backgroundColor}
224-
fontColor={fontColor}
225-
hasChildren={node.children !== undefined}
226-
/>
227-
);
228-
}).filter(Boolean)}
229-
</g>
230-
);
231-
})
232-
);
233-
}, [getModuleColor, nestedData, selectedNode]);
222+
return layers.map(({ key, values }) => {
223+
return (
224+
<g className="layer" key={key}>
225+
{values.map((node) => {
226+
const { backgroundColor, fontColor } = getModuleColor(node.data.getID());
227+
228+
if (node.x1 - node.x0 < 6 || node.y1 - node.y0 < 6) {
229+
return null;
230+
}
234231

235-
const tooltipVisible = useMemo(() => {
236-
return showTooltip && tooltipNode && tooltipPosition.every(v => v > 0);
237-
}, [showTooltip, tooltipNode, tooltipPosition]);
232+
return (
233+
<Node
234+
key={node.data.getID()}
235+
id={node.data.getID()}
236+
title={node.data.getName()}
237+
selected={selectedNodeID === node.data.getID()}
238+
x0={node.x0}
239+
y0={node.y0}
240+
x1={node.x1}
241+
y1={node.y1}
242+
243+
backgroundColor={backgroundColor}
244+
fontColor={fontColor}
245+
hasChildren={node.children !== undefined}
246+
/>
247+
);
248+
}).filter(Boolean)}
249+
</g>
250+
);
251+
});
252+
}, [getModuleColor, layers, selectedNodeID]);
238253

239254
return (
240255
<>
241-
{(tooltipVisible) && <Tooltip node={tooltipNode!.data} x={tooltipPosition[0]} y={tooltipPosition[1]} />}
256+
{(tooltipVisible) && <Tooltip node={tooltipNode!} x={clientX!} y={clientY!} />}
242257
<svg
243258
xmlns="http://www.w3.org/2000/svg"
244259
viewBox={`0 0 ${width} ${height}`}
245-
onMouseEnter={onMouseEnter}
246-
onMouseLeave={onMouseLeave}
247-
onMouseMove={onMouseMove}
248260
onClick={onClick}
261+
ref={svgRef}
249262
>
250263
{nodes}
251264
</svg>

0 commit comments

Comments
 (0)