Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: UI color enhance and performance optimize #49

Merged
merged 4 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ You can click to expand the package to see the details.
$ gsa --tui golang-compiled-binary
```

![image](https://github.com/Zxilly/go-size-analyzer/assets/31370133/04556f54-7ebb-42b8-ac57-91a17887a44e)
![image](https://github.com/Zxilly/go-size-analyzer/assets/31370133/e69583ce-b189-4a0d-b108-c3b7d5c33a82)

> [!NOTE]
> There may be a problem with the UI display on Windows, which is caused by dependent libraries.
Expand Down
2 changes: 1 addition & 1 deletion README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ $ gsa --web golang-compiled-binary

网页将如下所示:

![image](https://github.com/Zxilly/go-size-analyzer/assets/31370133/78bb8105-fc5a-4852-8704-8c2fac3bf475)
![image](https://github.com/Zxilly/go-size-analyzer/assets/31370133/e69583ce-b189-4a0d-b108-c3b7d5c33a82)

您可以点击以展开包以查看详细信息。

Expand Down
8 changes: 4 additions & 4 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@
"@types/d3-color": "^3.1.3",
"@types/d3-hierarchy": "^3.1.7",
"@types/d3-scale": "^4.0.8",
"@types/node": "^20.12.11",
"@types/node": "^20.12.12",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
"@vitejs/plugin-react-swc": "^3.6.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.6",
"eslint-plugin-react-refresh": "^0.4.7",
"lightningcss": "^1.24.1",
"sass": "^1.77.1",
"terser": "^5.31.0",
Expand Down
283 changes: 136 additions & 147 deletions ui/pnpm-lock.yaml

Large diffs are not rendered by default.

23 changes: 14 additions & 9 deletions ui/src/Node.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {HierarchyRectangularNode} from "d3-hierarchy";
import React, {useLayoutEffect, useMemo, useRef} from "react";
import React, {useEffect, useLayoutEffect, useMemo, useRef} from "react";
import {Entry} from "./tool/entry.ts";
import {NodeColorGetter} from "./tool/color.ts";
import {PADDING, TOP_PADDING} from "./tool/const.ts";
import {trimPrefix} from "./tool/utils.ts";
import {globalNodeCache} from "./cache.ts";

type NodeEventHandler = (event: HierarchyRectangularNode<Entry>) => void;

export interface NodeProps {
node: HierarchyRectangularNode<Entry>;
onMouseOver: NodeEventHandler;
selected: boolean;
onClick: NodeEventHandler;
getModuleColor: NodeColorGetter;
Expand All @@ -18,12 +18,20 @@ export interface NodeProps {
export const Node: React.FC<NodeProps> = (
{
node,
onMouseOver,
onClick,
selected,
getModuleColor
}
) => {
useEffect(() => {
globalNodeCache.set(node.data.getID(), node)

return () => {
// well, I think it won't be called :)
globalNodeCache.delete(node.data.getID())
}
}, [node]);

const {backgroundColor, fontColor} = getModuleColor(node);
const {x0, x1, y1, y0, children = null} = node;

Expand All @@ -35,13 +43,13 @@ export const Node: React.FC<NodeProps> = (

const textProps = useMemo(() => {
return {
"fontSize": "0.9em",
"fontSize": "0.8em",
"dominantBaseline": "middle",
"textAnchor": "middle",
x: width / 2,
y: children != null ? (TOP_PADDING + PADDING) / 2 : height / 2
}
}, [])
}, [children, height, width])

const title = useMemo(() => {
const t = trimPrefix(node.data.getName(), node.parent?.data.getName() ?? "")
Expand Down Expand Up @@ -94,10 +102,7 @@ export const Node: React.FC<NodeProps> = (
event.stopPropagation();
onClick(node);
}}
onMouseOver={(event) => {
event.stopPropagation();
onMouseOver(node);
}}
data-id={node.data.getID()}
>
<rect
fill={backgroundColor}
Expand Down
134 changes: 101 additions & 33 deletions ui/src/TreeMap.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import {loadData} from "./tool/utils.ts";
import {useCallback, useEffect, useMemo, useState} from "react";
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
import {Entry} from "./tool/entry.ts";
import {useWindowSize} from "usehooks-ts";
import {
hierarchy,
HierarchyNode,
HierarchyRectangularNode,
treemap,
treemapResquarify
} from "d3-hierarchy";
import {hierarchy, HierarchyNode, HierarchyRectangularNode, treemap, treemapResquarify} from "d3-hierarchy";
import {group} from "d3-array";
import createRainbowColor from "./tool/color.ts";
import {Tooltip} from "./Tooltip.tsx";
import {Node} from "./Node.tsx";
import {globalNodeCache} from "./cache.ts";

function TreeMap() {
const rootEntry = useMemo(() => new Entry(loadData()), [])
Expand All @@ -37,7 +32,6 @@ function TreeMap() {
return treemap<Entry>()
.size([width, height])
.paddingInner(2)
//.paddingOuter(2)
.paddingTop(20)
.round(true)
.tile(treemapResquarify);
Expand All @@ -60,21 +54,6 @@ function TreeMap() {
return selectedNodeLeaveSet.has(node) ? 1 : 0
}, [selectedNode, selectedNodeLeaveSet])

const [showTooltip, setShowTooltip] = useState<boolean>(false);
const [tooltipNode, setTooltipNode] = useState<
HierarchyRectangularNode<Entry> | undefined
>(undefined);

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

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

const root = useMemo(() => {
const rootWithSizesAndSorted = rawHierarchy
Expand Down Expand Up @@ -106,10 +85,104 @@ function TreeMap() {
return nestedData;
}, [root]);

const [showTooltip, setShowTooltip] = useState(false);
const [tooltipNode, setTooltipNode] =
useState<HierarchyRectangularNode<Entry> | undefined>(undefined);

const svgRef = useRef<SVGSVGElement>(null);

useEffect(() => {
if (!svgRef.current) {
return;
}
const svg = svgRef.current;

const visibleListener = (value: boolean) => {
return () => {
setShowTooltip(value);
}
}
const enter = visibleListener(true);
const leave = visibleListener(false);

svg.addEventListener("mouseenter", enter);
svg.addEventListener("mouseleave", leave);

return () => {
svg.removeEventListener("mouseenter", enter);
svg.removeEventListener("mouseleave", leave);
}
}, []);

useEffect(() => {
const moveListener = (e: MouseEvent) => {
if (!e.target) {
return;
}

const target = (e.target as SVGElement).parentNode;
if (!target) {
return;
}

const dataIdStr = (target as Element).getAttribute("data-id");
if (!dataIdStr) {
return;
}

const dataId = parseInt(dataIdStr);

const node = globalNodeCache.get(dataId);
if (!node) {
return;
}

setTooltipNode(node);
}

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

const nodes = useMemo(() => {
return (
<Nodes
nestedData={nestedData}
selectedNode={selectedNode}
getModuleColor={getModuleColor}
setSelectedNode={setSelectedNode}
/>
)
}, [getModuleColor, nestedData, selectedNode])

return (
<>
<Tooltip visible={showTooltip} node={tooltipNode}/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${width} ${height}`}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${width} ${height}`} ref={svgRef}>
{nodes}
</svg>
</>
)
}

interface NodesProps {
nestedData: { key: number, values: HierarchyRectangularNode<Entry>[] }[]
selectedNode: HierarchyRectangularNode<Entry> | null
getModuleColor: (node: HierarchyNode<Entry>) => { backgroundColor: string, fontColor: string }
setSelectedNode: (node: HierarchyRectangularNode<Entry> | null) => void
}

const Nodes: React.FC<NodesProps> =
({
nestedData,
selectedNode,
getModuleColor,
setSelectedNode
}) => {
return (
<>
{nestedData.map(({key, values}) => {
return (
<g className="layer" key={key}>
Expand All @@ -118,10 +191,6 @@ function TreeMap() {
<Node
key={node.data.getID()}
node={node}
onMouseOver={(node) => {
setTooltipNode(node);
setShowTooltip(true);
}}
selected={selectedNode?.data?.getID() === node.data.getID()}
onClick={(node) => {
setSelectedNode(selectedNode?.data?.getID() === node.data.getID() ? null : node);
Expand All @@ -133,9 +202,8 @@ function TreeMap() {
</g>
);
})}
</svg>
</>
)
}
</>
)
}

export default TreeMap
4 changes: 4 additions & 0 deletions ui/src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {Entry} from "./tool/entry.ts";
import {HierarchyRectangularNode} from "d3-hierarchy";

export const globalNodeCache = new Map<number, HierarchyRectangularNode<Entry>>
1 change: 0 additions & 1 deletion ui/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,4 @@ svg {

.node {
cursor: pointer;
margin: 2px;
}
15 changes: 6 additions & 9 deletions ui/src/tool/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ import {
Section,
Symbol as FileSymbol
} from "../generated/schema.ts";
import {id} from "./id.ts";
import {orderedID} from "./id.ts";
import {formatBytes, title} from "./utils.ts";
import {max} from "d3-array";

type Candidate = Section | File | Package | Result | FileSymbol;

type EntryType = "section" | "file" | "package" | "result" | "symbol" | "disasm" | "unknown" | "container";
export type EntryType = "section" | "file" | "package" | "result" | "symbol" | "disasm" | "unknown" | "container";

export class Entry {
private readonly type: EntryType;
private readonly data?: Candidate;
private readonly size: number;
private readonly name: string;
private readonly children: Entry[] = [];
private readonly uid = id();
private readonly uid = orderedID();
explain: string = ""; // should only be used by the container type

constructor(data: Candidate)
Expand Down Expand Up @@ -198,8 +198,8 @@ export class Entry {
return this.children;
}

public getID(): string {
return this.uid.toString(16);
public getID(): number {
return this.uid;
}
}

Expand Down Expand Up @@ -255,10 +255,7 @@ function childrenFromResult(result: Result): Entry[] {
packageContainer.explain = `The size of the ${type} packages in the binary.`
typedPackagesChildren.push(packageContainer);
}
const packageContainerSize = typedPackagesChildren.reduce((acc, child) => acc + child.getSize(), 0);
const packageContainer = new Entry("Packages Size", packageContainerSize, "container", typedPackagesChildren);
packageContainer.explain = "The size of the packages in the binary."
children.push(packageContainer);
children.push(...typedPackagesChildren);

const leftSize = result.size - children.reduce((acc, child) => acc + child.getSize(), 0);
if (leftSize > 0) {
Expand Down
4 changes: 3 additions & 1 deletion ui/src/tool/id.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
let count = 1;

export function id() {
export function orderedID() {
return count++;
}


17 changes: 17 additions & 0 deletions ui/src/tool/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Result} from "../schema/schema.ts";
import {parseResult} from "../generated/schema.ts";
import {useCallback, useRef} from 'react';

export function loadData(): Result {
const doc = document.querySelector("#data")!;
Expand Down Expand Up @@ -31,3 +32,19 @@ export function trimPrefix(str: string, prefix: string) {
return str
}
}

export function useThrottle<T extends (...args: Parameters<T>) => ReturnType<T>>(func: T, delay: number): (...args: Parameters<T>) => void {
const lastCall = useRef<number>(0);
const lastFunc = useRef<T>(func);

lastFunc.current = func;

return useCallback((...args: Parameters<T>) => {
const now = Date.now();

if (now - lastCall.current >= delay) {
lastCall.current = now;
lastFunc.current(...args);
}
}, [delay]);
}
Loading