@parca/profile 0.16.136 → 0.16.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/Callgraph/index.d.ts +3 -3
- package/dist/Callgraph/index.js +79 -191
- package/dist/Callgraph/utils.js +15 -19
- package/dist/GraphTooltip/index.d.ts +22 -8
- package/dist/GraphTooltip/index.js +14 -14
- package/dist/ProfileIcicleGraph/IcicleGraph/IcicleGraphNodes.js +2 -1
- package/dist/ProfileView/index.js +113 -11
- package/dist/index.d.ts +1 -1
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/Callgraph/index.tsx +127 -323
- package/src/Callgraph/utils.ts +15 -21
- package/src/GraphTooltip/index.tsx +38 -13
- package/src/ProfileIcicleGraph/IcicleGraph/IcicleGraphNodes.tsx +2 -1
- package/src/ProfileView/index.tsx +72 -19
- package/typings.d.ts +1 -0
package/src/Callgraph/index.tsx
CHANGED
|
@@ -15,355 +15,159 @@ import {useEffect, useRef, useState} from 'react';
|
|
|
15
15
|
|
|
16
16
|
import cx from 'classnames';
|
|
17
17
|
import * as d3 from 'd3';
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import {Arrow, Label, Layer, Rect, Stage, Text} from 'react-konva';
|
|
18
|
+
import SVG from 'react-inlinesvg';
|
|
19
|
+
import {MapInteractionCSS} from 'react-map-interaction';
|
|
21
20
|
|
|
22
|
-
import {CallgraphEdge,
|
|
23
|
-
import {Button, useURLState} from '@parca/components';
|
|
24
|
-
import {
|
|
21
|
+
import {CallgraphEdge, Callgraph as CallgraphType} from '@parca/client';
|
|
22
|
+
import {Button, useKeyDown, useURLState} from '@parca/components';
|
|
23
|
+
import {getNewSpanColor} from '@parca/functions';
|
|
24
|
+
import {selectDarkMode, setHoveringNode, useAppDispatch, useAppSelector} from '@parca/store';
|
|
25
25
|
|
|
26
|
-
import
|
|
27
|
-
import {DEFAULT_NODE_HEIGHT, GRAPH_MARGIN} from './constants';
|
|
28
|
-
import {getCurvePoints, jsonToDot} from './utils';
|
|
26
|
+
import GraphTooltip from '../GraphTooltip';
|
|
29
27
|
|
|
30
|
-
interface NodeProps {
|
|
31
|
-
node: INode;
|
|
32
|
-
hoveredNode: INode | null;
|
|
33
|
-
setHoveredNode: (node: INode | null) => void;
|
|
34
|
-
isCurrentSearchMatch: boolean;
|
|
35
|
-
}
|
|
36
|
-
interface EdgeProps {
|
|
37
|
-
edge: GraphvizEdge;
|
|
38
|
-
sourceNode: {x: number; y: number};
|
|
39
|
-
targetNode: {x: number; y: number};
|
|
40
|
-
xScale: (x: number) => number;
|
|
41
|
-
yScale: (y: number) => number;
|
|
42
|
-
isCurrentSearchMatch: boolean;
|
|
43
|
-
}
|
|
44
28
|
export interface Props {
|
|
45
|
-
|
|
29
|
+
data: CallgraphType;
|
|
30
|
+
svgString: string;
|
|
46
31
|
sampleUnit: string;
|
|
47
32
|
width: number;
|
|
48
|
-
colorRange: [string, string];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface GraphvizNode extends CallgraphNode {
|
|
52
|
-
_gvid: number;
|
|
53
|
-
name: string;
|
|
54
|
-
pos: string;
|
|
55
|
-
functionName: string;
|
|
56
|
-
color: string;
|
|
57
|
-
width: string | number;
|
|
58
|
-
height: string | number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface INode extends GraphvizNode {
|
|
62
|
-
x: number;
|
|
63
|
-
y: number;
|
|
64
|
-
data: {id: string};
|
|
65
|
-
mouseX?: number;
|
|
66
|
-
mouseY?: number;
|
|
67
33
|
}
|
|
68
34
|
|
|
69
|
-
interface
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
head: number;
|
|
73
|
-
pos: string;
|
|
74
|
-
color: string;
|
|
75
|
-
opacity: string;
|
|
76
|
-
boxHeight: number;
|
|
35
|
+
interface View {
|
|
36
|
+
scale: number;
|
|
37
|
+
translation: {x: number; y: number};
|
|
77
38
|
}
|
|
78
39
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
height: heightString,
|
|
99
|
-
} = node;
|
|
100
|
-
const isHovered = Boolean(hoveredNode) && hoveredNode?.data.id === id;
|
|
101
|
-
const width = Number(widthString);
|
|
102
|
-
const height = Number(heightString);
|
|
103
|
-
const textPadding = 6;
|
|
104
|
-
const opacity = isCurrentSearchMatch ? 1 : 0.1;
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<Label x={x - width / 2} y={y - height / 2}>
|
|
108
|
-
<Rect
|
|
109
|
-
width={width}
|
|
110
|
-
height={height}
|
|
111
|
-
fill={color}
|
|
112
|
-
opacity={opacity}
|
|
113
|
-
cornerRadius={3}
|
|
114
|
-
stroke={isHovered ? 'black' : color}
|
|
115
|
-
strokeWidth={2}
|
|
116
|
-
onMouseOver={e => {
|
|
117
|
-
setHoveredNode({...node, mouseX: e.evt.clientX, mouseY: e.evt.clientY});
|
|
118
|
-
}}
|
|
119
|
-
onMouseOut={() => {
|
|
120
|
-
setHoveredNode(null);
|
|
121
|
-
}}
|
|
122
|
-
/>
|
|
123
|
-
{width > DEFAULT_NODE_HEIGHT + 10 && (
|
|
124
|
-
<Text
|
|
125
|
-
text={functionName}
|
|
126
|
-
fontSize={10}
|
|
127
|
-
fill="white"
|
|
128
|
-
width={width - textPadding}
|
|
129
|
-
height={height - textPadding}
|
|
130
|
-
x={textPadding / 2}
|
|
131
|
-
y={textPadding / 2}
|
|
132
|
-
align="center"
|
|
133
|
-
verticalAlign="middle"
|
|
134
|
-
listening={false}
|
|
135
|
-
/>
|
|
136
|
-
)}
|
|
137
|
-
</Label>
|
|
138
|
-
);
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const Edge = ({
|
|
142
|
-
edge,
|
|
143
|
-
sourceNode,
|
|
144
|
-
targetNode,
|
|
145
|
-
xScale,
|
|
146
|
-
yScale,
|
|
147
|
-
isCurrentSearchMatch,
|
|
148
|
-
}: EdgeProps): JSX.Element => {
|
|
149
|
-
const {pos, color, head, tail, opacity, boxHeight} = edge;
|
|
150
|
-
|
|
151
|
-
const points = getCurvePoints({
|
|
152
|
-
pos,
|
|
153
|
-
xScale,
|
|
154
|
-
yScale,
|
|
155
|
-
source: [sourceNode.x, sourceNode.y],
|
|
156
|
-
target: [targetNode.x, targetNode.y],
|
|
157
|
-
offset: boxHeight / 2,
|
|
158
|
-
isSelfLoop: head === tail,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
return (
|
|
162
|
-
<Arrow
|
|
163
|
-
points={points}
|
|
164
|
-
bezier={true}
|
|
165
|
-
stroke={color}
|
|
166
|
-
strokeWidth={3}
|
|
167
|
-
pointerLength={10}
|
|
168
|
-
pointerWidth={10}
|
|
169
|
-
fill={color}
|
|
170
|
-
opacity={isCurrentSearchMatch ? Number(opacity) : 0}
|
|
171
|
-
/>
|
|
172
|
-
);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const Callgraph = ({graph, sampleUnit, width, colorRange}: Props): JSX.Element => {
|
|
176
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
177
|
-
const [graphData, setGraphData] = useState<any>(null);
|
|
178
|
-
const [hoveredNode, setHoveredNode] = useState<INode | null>(null);
|
|
179
|
-
const [stage, setStage] = useState<{scale: {x: number; y: number}; x: number; y: number}>({
|
|
180
|
-
scale: {x: 1, y: 1},
|
|
181
|
-
x: 0,
|
|
182
|
-
y: 0,
|
|
183
|
-
});
|
|
184
|
-
const {nodes: rawNodes, cumulative: total} = graph;
|
|
185
|
-
const currentSearchString = (selectQueryParam('search_string') as string) ?? '';
|
|
186
|
-
const isSearchEmpty = currentSearchString === undefined || currentSearchString === '';
|
|
40
|
+
const Callgraph = ({data, svgString, sampleUnit, width}: Props): JSX.Element => {
|
|
41
|
+
const originalView = {
|
|
42
|
+
scale: 1,
|
|
43
|
+
translation: {x: 0, y: 0},
|
|
44
|
+
};
|
|
45
|
+
const [view, setView] = useState<View>(originalView);
|
|
46
|
+
const containerRef = useRef(null);
|
|
47
|
+
const svgRef = useRef(null);
|
|
48
|
+
const svgWrapper = useRef(null);
|
|
49
|
+
const [svgWrapperLoaded, setSvgWrapperLoaded] = useState(false);
|
|
50
|
+
const dispatch = useAppDispatch();
|
|
51
|
+
const {isShiftDown} = useKeyDown();
|
|
52
|
+
// TODO: implement highlighting nodes on user search
|
|
53
|
+
// const currentSearchString = (selectQueryParam('search_string') as string) ?? '';
|
|
54
|
+
// const isSearchEmpty = currentSearchString === undefined || currentSearchString === '';
|
|
55
|
+
// const isCurrentSearchMatch = isSearchEmpty
|
|
56
|
+
// ? true
|
|
57
|
+
// : isSearchMatch(currentSearchString, sourceNode.functionName) &&
|
|
58
|
+
// isSearchMatch(currentSearchString, targetNode.functionName);
|
|
187
59
|
const [rawDashboardItems] = useURLState({param: 'dashboard_items'});
|
|
188
|
-
|
|
189
60
|
const dashboardItems =
|
|
190
61
|
rawDashboardItems !== undefined ? (rawDashboardItems as string[]) : ['icicle'];
|
|
191
62
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
await graphviz.loadWASM(); // need to load the WASM instance and wait for it
|
|
63
|
+
const isDarkMode = useAppSelector(selectDarkMode);
|
|
64
|
+
const maxColor: string = getNewSpanColor(isDarkMode);
|
|
65
|
+
const minColor: string = d3.scaleLinear([isDarkMode ? 'black' : 'white', maxColor])(0.3);
|
|
66
|
+
const colorRange: [string, string] = [minColor, maxColor];
|
|
67
|
+
const cumulatives = data.edges.map((edge: CallgraphEdge) => parseInt(edge.cumulative));
|
|
68
|
+
const cumulativesRange = d3.extent(cumulatives);
|
|
69
|
+
const colorScale = d3
|
|
70
|
+
.scaleSequentialLog(d3.interpolateBlues)
|
|
71
|
+
.domain([Number(cumulativesRange[0]), Number(cumulativesRange[1])])
|
|
72
|
+
.range(colorRange);
|
|
203
73
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
if (width !== null) {
|
|
210
|
-
void getDataWithPositions();
|
|
211
|
-
}
|
|
212
|
-
}, [graph, width, colorRange]);
|
|
213
|
-
|
|
214
|
-
// 3. Render the graph with calculated layout in Canvas container
|
|
215
|
-
if (width == null || graphData == null) return <></>;
|
|
216
|
-
const {objects: gvizNodes, edges, bb: boundingBox} = JSON.parse(graphData) as GraphvizType;
|
|
217
|
-
|
|
218
|
-
if (gvizNodes.length < 1) return <>Profile has no samples</>;
|
|
219
|
-
|
|
220
|
-
const graphBB = boundingBox.split(',');
|
|
221
|
-
const bbWidth = Number(graphBB[2]);
|
|
222
|
-
const bbHeight = Number(graphBB[3]);
|
|
223
|
-
const height = (width * bbHeight) / bbWidth;
|
|
224
|
-
const xScale = d3
|
|
225
|
-
.scaleLinear()
|
|
226
|
-
.domain([0, bbWidth])
|
|
227
|
-
.range([0, width - 2 * GRAPH_MARGIN]);
|
|
228
|
-
const yScale = d3
|
|
229
|
-
.scaleLinear()
|
|
230
|
-
.domain([0, bbHeight])
|
|
231
|
-
.range([0, height - 2 * GRAPH_MARGIN]);
|
|
232
|
-
|
|
233
|
-
const nodes: INode[] = gvizNodes.map((node: GraphvizNode) => {
|
|
234
|
-
const [x, y] = node.pos.split(',');
|
|
235
|
-
return {
|
|
236
|
-
...node,
|
|
237
|
-
x: xScale(Number(x)),
|
|
238
|
-
y: yScale(Number(y)),
|
|
239
|
-
data: rawNodes.find(n => n.id === node.name) ?? {id: 'n0'},
|
|
240
|
-
};
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// 4. Add zooming
|
|
244
|
-
const handleWheel: (e: KonvaEventObject<WheelEvent>) => void = e => {
|
|
245
|
-
// stop default scrolling
|
|
246
|
-
e.evt.preventDefault();
|
|
247
|
-
|
|
248
|
-
const scaleBy = 1.01;
|
|
249
|
-
const stage = e.target.getStage();
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
setSvgWrapperLoaded(true);
|
|
76
|
+
}, []);
|
|
250
77
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (svgWrapperLoaded && svgRef.current !== null) {
|
|
80
|
+
const addInteraction = (): void => {
|
|
81
|
+
const svg = d3.select(svgRef.current);
|
|
82
|
+
const nodes = svg.selectAll('.node');
|
|
83
|
+
|
|
84
|
+
nodes.each(function () {
|
|
85
|
+
const nodeData = data.nodes.find((n): boolean => {
|
|
86
|
+
// @ts-expect-error
|
|
87
|
+
return n.id === this.id;
|
|
88
|
+
});
|
|
89
|
+
const defaultColor = colorScale(Number(nodeData?.cumulative));
|
|
90
|
+
const node = d3.select(this);
|
|
91
|
+
const path = node.select('path');
|
|
92
|
+
|
|
93
|
+
node
|
|
94
|
+
.style('cursor', 'pointer')
|
|
95
|
+
.on('mouseenter', function () {
|
|
96
|
+
if (isShiftDown) return;
|
|
97
|
+
d3.select(this).select('path').style('fill', 'white');
|
|
98
|
+
const hoveringNode = {
|
|
99
|
+
...nodeData,
|
|
100
|
+
meta: {...nodeData?.meta, lineIndex: 0, locationIndex: 0},
|
|
101
|
+
};
|
|
102
|
+
// @ts-expect-error
|
|
103
|
+
dispatch(setHoveringNode(hoveringNode));
|
|
104
|
+
})
|
|
105
|
+
.on('mouseleave', function () {
|
|
106
|
+
if (isShiftDown) return;
|
|
107
|
+
d3.select(this).select('path').style('fill', defaultColor);
|
|
108
|
+
dispatch(setHoveringNode(undefined));
|
|
109
|
+
});
|
|
110
|
+
path.style('fill', defaultColor);
|
|
111
|
+
});
|
|
257
112
|
};
|
|
258
113
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (e.evt.ctrlKey) {
|
|
264
|
-
direction = -direction;
|
|
265
|
-
}
|
|
114
|
+
setTimeout(addInteraction, 1000);
|
|
115
|
+
}
|
|
116
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
117
|
+
}, [svgWrapper.current, svgWrapperLoaded]);
|
|
266
118
|
|
|
267
|
-
|
|
268
|
-
stage.scale({x: newScale, y: newScale});
|
|
119
|
+
if (data.nodes.length < 1) return <>Profile has no samples</>;
|
|
269
120
|
|
|
270
|
-
|
|
271
|
-
scale: {x: newScale, y: newScale},
|
|
272
|
-
x: -(mousePointTo.x - pointer.x / newScale) * newScale,
|
|
273
|
-
y: -(mousePointTo.y - pointer.y / newScale) * newScale,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
};
|
|
121
|
+
const resetView = (): void => setView(originalView);
|
|
277
122
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
x: 0,
|
|
283
|
-
y: 0,
|
|
284
|
-
});
|
|
285
|
-
};
|
|
123
|
+
const isResetViewButtonEnabled =
|
|
124
|
+
view.scale !== originalView.scale ||
|
|
125
|
+
view.translation.x !== originalView.translation.x ||
|
|
126
|
+
view.translation.y !== originalView.translation.y;
|
|
286
127
|
|
|
287
128
|
return (
|
|
288
|
-
<div className="relative">
|
|
289
|
-
<div className=
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
x={stage.x}
|
|
297
|
-
y={stage.y}
|
|
298
|
-
draggable
|
|
129
|
+
<div className="w-full relative">
|
|
130
|
+
<div ref={containerRef} className="w-full overflow-hidden">
|
|
131
|
+
<MapInteractionCSS
|
|
132
|
+
showControls
|
|
133
|
+
minScale={1}
|
|
134
|
+
maxScale={5}
|
|
135
|
+
value={view}
|
|
136
|
+
onChange={(value: View) => setView(value)}
|
|
299
137
|
>
|
|
300
|
-
<
|
|
301
|
-
{
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
isSearchMatch(currentSearchString, targetNode.functionName);
|
|
317
|
-
return (
|
|
318
|
-
<Edge
|
|
319
|
-
key={`edge-${edge.tail}-${edge.head}`}
|
|
320
|
-
edge={edge}
|
|
321
|
-
xScale={xScale}
|
|
322
|
-
yScale={yScale}
|
|
323
|
-
sourceNode={sourceNode}
|
|
324
|
-
targetNode={targetNode}
|
|
325
|
-
isCurrentSearchMatch={isCurrentSearchMatch}
|
|
326
|
-
/>
|
|
327
|
-
);
|
|
328
|
-
})}
|
|
329
|
-
{nodes.map(node => {
|
|
330
|
-
const isCurrentSearchMatch = isSearchEmpty
|
|
331
|
-
? true
|
|
332
|
-
: isSearchMatch(currentSearchString, node.functionName);
|
|
333
|
-
return (
|
|
334
|
-
<Node
|
|
335
|
-
key={`node-${node._gvid}`}
|
|
336
|
-
node={node}
|
|
337
|
-
hoveredNode={hoveredNode}
|
|
338
|
-
setHoveredNode={setHoveredNode}
|
|
339
|
-
isCurrentSearchMatch={isCurrentSearchMatch}
|
|
340
|
-
/>
|
|
341
|
-
);
|
|
342
|
-
})}
|
|
343
|
-
</Layer>
|
|
344
|
-
</Stage>
|
|
345
|
-
<Tooltip
|
|
346
|
-
hoveringNode={rawNodes.find(n => n.id === hoveredNode?.data.id) as HoveringNode}
|
|
347
|
-
unit={sampleUnit}
|
|
348
|
-
total={+total}
|
|
349
|
-
isFixed={false}
|
|
350
|
-
x={hoveredNode?.mouseX ?? 0}
|
|
351
|
-
y={hoveredNode?.mouseY ?? 0}
|
|
352
|
-
contextElement={containerRef.current}
|
|
353
|
-
/>
|
|
354
|
-
{stage.scale.x !== 1 && (
|
|
355
|
-
<div
|
|
356
|
-
className={cx(
|
|
357
|
-
dashboardItems.length > 1 ? 'left-[25px]' : 'left-0',
|
|
358
|
-
'w-auto absolute top-[-46px]'
|
|
359
|
-
)}
|
|
360
|
-
>
|
|
361
|
-
<Button variant="neutral" onClick={resetZoom}>
|
|
362
|
-
Reset Zoom
|
|
363
|
-
</Button>
|
|
364
|
-
</div>
|
|
138
|
+
<SVG
|
|
139
|
+
ref={svgWrapper}
|
|
140
|
+
src={svgString}
|
|
141
|
+
width={width}
|
|
142
|
+
height="auto"
|
|
143
|
+
title="Callgraph"
|
|
144
|
+
innerRef={svgRef}
|
|
145
|
+
/>
|
|
146
|
+
</MapInteractionCSS>
|
|
147
|
+
{svgRef.current !== null && (
|
|
148
|
+
<GraphTooltip
|
|
149
|
+
type="callgraph"
|
|
150
|
+
unit={sampleUnit}
|
|
151
|
+
total={parseInt(data.cumulative)}
|
|
152
|
+
contextElement={containerRef.current}
|
|
153
|
+
/>
|
|
365
154
|
)}
|
|
366
155
|
</div>
|
|
156
|
+
<div
|
|
157
|
+
className={cx(
|
|
158
|
+
dashboardItems.length > 1 ? 'left-[25px]' : 'left-0',
|
|
159
|
+
'w-auto absolute top-[-46px]'
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
<Button
|
|
163
|
+
variant="neutral"
|
|
164
|
+
onClick={resetView}
|
|
165
|
+
className="z-50"
|
|
166
|
+
disabled={!isResetViewButtonEnabled}
|
|
167
|
+
>
|
|
168
|
+
Reset View
|
|
169
|
+
</Button>
|
|
170
|
+
</div>
|
|
367
171
|
</div>
|
|
368
172
|
);
|
|
369
173
|
};
|
package/src/Callgraph/utils.ts
CHANGED
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
14
|
import * as d3 from 'd3';
|
|
15
|
+
import {withAlphaHex} from 'with-alpha-hex';
|
|
15
16
|
|
|
16
17
|
import {CallgraphEdge, CallgraphNode} from '@parca/client';
|
|
17
18
|
|
|
18
|
-
import {DEFAULT_NODE_HEIGHT} from './constants';
|
|
19
|
-
|
|
20
19
|
export const pixelsToInches = (pixels: number): number => pixels / 96;
|
|
21
20
|
|
|
22
21
|
export const getCurvePoints = ({
|
|
@@ -94,28 +93,23 @@ export const jsonToDot = ({
|
|
|
94
93
|
colorRange: [string, string];
|
|
95
94
|
}): string => {
|
|
96
95
|
const {nodes, edges} = graph;
|
|
97
|
-
const cumulatives =
|
|
98
|
-
const cumulativesRange = d3.extent(cumulatives)
|
|
99
|
-
|
|
96
|
+
const cumulatives = edges.map((edge: CallgraphEdge) => Number(edge.cumulative));
|
|
97
|
+
const cumulativesRange = d3.extent(cumulatives) as [number, number];
|
|
100
98
|
const colorScale = d3
|
|
101
99
|
.scaleSequentialLog(d3.interpolateBlues)
|
|
102
100
|
.domain(cumulativesRange)
|
|
103
101
|
.range(colorRange);
|
|
104
|
-
const colorOpacityScale = d3.
|
|
105
|
-
const boxWidthScale = d3
|
|
106
|
-
.scaleLog()
|
|
107
|
-
.domain(cumulativesRange)
|
|
108
|
-
.range([DEFAULT_NODE_HEIGHT, DEFAULT_NODE_HEIGHT + 40]);
|
|
102
|
+
const colorOpacityScale = d3.scaleLinear().domain(cumulativesRange).range([0.5, 1]);
|
|
109
103
|
|
|
110
104
|
const nodesAsStrings = nodes.map((node: CallgraphNode) => {
|
|
105
|
+
const rgbColor = colorScale(Number(node.cumulative));
|
|
106
|
+
const hexColor = d3.color(rgbColor)?.formatHex() ?? 'red';
|
|
111
107
|
const dataAttributes = {
|
|
112
|
-
|
|
113
|
-
functionName: node.meta?.function?.name ?? '',
|
|
114
|
-
cumulative: node.cumulative ?? '',
|
|
108
|
+
label: node.meta?.function?.name.substring(0, 12) ?? '',
|
|
115
109
|
root: (node.id === 'root').toString(),
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
fillcolor: hexColor,
|
|
111
|
+
className: 'node',
|
|
112
|
+
id: node.id,
|
|
119
113
|
};
|
|
120
114
|
|
|
121
115
|
return `"${node.id}" [${objectAsDotAttributes(dataAttributes)}]`;
|
|
@@ -124,21 +118,21 @@ export const jsonToDot = ({
|
|
|
124
118
|
const edgesAsStrings = edges.map((edge: CallgraphEdge) => {
|
|
125
119
|
const dataAttributes = {
|
|
126
120
|
cumulative: edge.cumulative,
|
|
127
|
-
color: colorRange[1],
|
|
128
|
-
|
|
129
|
-
boxHeight: DEFAULT_NODE_HEIGHT,
|
|
121
|
+
color: withAlphaHex(colorRange[1], colorOpacityScale(Number(edge.cumulative))),
|
|
122
|
+
className: 'edge',
|
|
123
|
+
// boxHeight: DEFAULT_NODE_HEIGHT,
|
|
130
124
|
};
|
|
131
125
|
|
|
132
126
|
return `"${edge.source}" -> "${edge.target}" [${objectAsDotAttributes(dataAttributes)}]`;
|
|
133
127
|
});
|
|
134
128
|
|
|
135
129
|
const graphAsDot = `digraph "callgraph" {
|
|
136
|
-
rankdir="
|
|
130
|
+
rankdir="TB"
|
|
137
131
|
overlap="prism"
|
|
138
132
|
ratio="1,3"
|
|
139
133
|
margin=15
|
|
140
134
|
edge [margin=0]
|
|
141
|
-
node [shape=box style=rounded
|
|
135
|
+
node [shape=box style="rounded,filled"]
|
|
142
136
|
${nodesAsStrings.join(' ')}
|
|
143
137
|
${edgesAsStrings.join(' ')}
|
|
144
138
|
}`;
|