@parca/profile 0.7.10
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 +60 -0
- package/LICENSE +201 -0
- package/README.md +1 -0
- package/package.json +23 -0
- package/src/IcicleGraph.tsx +544 -0
- package/src/ProfileIcicleGraph.stories.mdx +31 -0
- package/src/ProfileIcicleGraph.tsx +19 -0
- package/src/ProfileSVG.module.css +3 -0
- package/src/ProfileSource.tsx +327 -0
- package/src/ProfileView.tsx +164 -0
- package/src/__tests__/suffix_params.test.ts +11 -0
- package/src/index.tsx +4 -0
- package/src/testdata/fg-diff.json +3750 -0
- package/src/testdata/fg-simple.json +1879 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import React, {MouseEvent, useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {throttle} from 'lodash';
|
|
3
|
+
import {pointer} from 'd3-selection';
|
|
4
|
+
import {scaleLinear} from 'd3-scale';
|
|
5
|
+
import {Flamegraph, FlamegraphNode, FlamegraphRootNode} from '@parca/client';
|
|
6
|
+
import {usePopper} from 'react-popper';
|
|
7
|
+
import {valueFormatter} from '@parca/functions';
|
|
8
|
+
|
|
9
|
+
const RowHeight = 20;
|
|
10
|
+
|
|
11
|
+
const icicleRectStyles = {
|
|
12
|
+
cursor: 'pointer',
|
|
13
|
+
transition: 'opacity .15s linear',
|
|
14
|
+
};
|
|
15
|
+
const fadedIcicleRectStyles = {
|
|
16
|
+
cursor: 'pointer',
|
|
17
|
+
transition: 'opacity .15s linear',
|
|
18
|
+
opacity: '0.5',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface IcicleRectProps {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
color: string;
|
|
27
|
+
name: string;
|
|
28
|
+
onMouseEnter: (e: MouseEvent) => void;
|
|
29
|
+
onMouseLeave: (e: MouseEvent) => void;
|
|
30
|
+
onClick: (e: MouseEvent) => void;
|
|
31
|
+
curPath: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function IcicleRect({
|
|
35
|
+
x,
|
|
36
|
+
y,
|
|
37
|
+
width,
|
|
38
|
+
height,
|
|
39
|
+
color,
|
|
40
|
+
name,
|
|
41
|
+
onMouseEnter,
|
|
42
|
+
onMouseLeave,
|
|
43
|
+
onClick,
|
|
44
|
+
curPath,
|
|
45
|
+
}: IcicleRectProps) {
|
|
46
|
+
const isFaded = curPath.length > 0 && name !== curPath[curPath.length - 1];
|
|
47
|
+
const styles = isFaded ? fadedIcicleRectStyles : icicleRectStyles;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<g
|
|
51
|
+
transform={`translate(${x + 1}, ${y + 1})`}
|
|
52
|
+
style={styles}
|
|
53
|
+
onMouseEnter={onMouseEnter}
|
|
54
|
+
onMouseLeave={onMouseLeave}
|
|
55
|
+
onClick={onClick}
|
|
56
|
+
>
|
|
57
|
+
<rect
|
|
58
|
+
x={0}
|
|
59
|
+
y={0}
|
|
60
|
+
width={width - 1}
|
|
61
|
+
height={height - 1}
|
|
62
|
+
style={{
|
|
63
|
+
fill: color,
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
66
|
+
{width > 5 && (
|
|
67
|
+
<svg width={width - 5} height={height}>
|
|
68
|
+
<text x={5} y={13} style={{fontSize: '12px'}}>
|
|
69
|
+
{name}
|
|
70
|
+
</text>
|
|
71
|
+
</svg>
|
|
72
|
+
)}
|
|
73
|
+
</g>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface IcicleGraphNodesProps {
|
|
78
|
+
data: FlamegraphNode.AsObject[];
|
|
79
|
+
x: number;
|
|
80
|
+
y: number;
|
|
81
|
+
total: number;
|
|
82
|
+
totalWidth: number;
|
|
83
|
+
level: number;
|
|
84
|
+
curPath: string[];
|
|
85
|
+
setCurPath: (path: string[]) => void;
|
|
86
|
+
setHoveringNode: (
|
|
87
|
+
node: FlamegraphNode.AsObject | FlamegraphRootNode.AsObject | undefined
|
|
88
|
+
) => void;
|
|
89
|
+
path: string[];
|
|
90
|
+
xScale: (value: number) => number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function diffColor(diff: number, cumulative: number): string {
|
|
94
|
+
const prevValue = cumulative - diff;
|
|
95
|
+
const diffRatio = prevValue > 0 ? (Math.abs(diff) > 0 ? diff / prevValue : 0) : 1.0;
|
|
96
|
+
|
|
97
|
+
const diffTransparency =
|
|
98
|
+
Math.abs(diff) > 0 ? Math.min((Math.abs(diffRatio) / 2 + 0.5) * 0.8, 0.8) : 0;
|
|
99
|
+
const color =
|
|
100
|
+
diff === 0
|
|
101
|
+
? '#90c7e0'
|
|
102
|
+
: diff > 0
|
|
103
|
+
? `rgba(221, 46, 69, ${diffTransparency})`
|
|
104
|
+
: `rgba(59, 165, 93, ${diffTransparency})`;
|
|
105
|
+
|
|
106
|
+
return color;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getLastItem(thePath: string): string {
|
|
110
|
+
const index = thePath.lastIndexOf('/');
|
|
111
|
+
if (index === -1) return thePath;
|
|
112
|
+
|
|
113
|
+
return thePath.substring(index + 1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function nodeLabel(node: FlamegraphNode.AsObject): string {
|
|
117
|
+
if (node.meta === undefined) return '<unknown>';
|
|
118
|
+
const mapping = `${
|
|
119
|
+
node.meta?.mapping?.file !== undefined && node.meta?.mapping?.file !== ''
|
|
120
|
+
? '[' + getLastItem(node.meta.mapping.file) + '] '
|
|
121
|
+
: ''
|
|
122
|
+
}`;
|
|
123
|
+
if (node.meta.pb_function?.name !== undefined && node.meta.pb_function?.name !== '')
|
|
124
|
+
return mapping + node.meta.pb_function.name;
|
|
125
|
+
|
|
126
|
+
const address = `${
|
|
127
|
+
node.meta.location?.address !== undefined && node.meta.location?.address !== 0
|
|
128
|
+
? '0x' + node.meta.location.address.toString(16)
|
|
129
|
+
: ''
|
|
130
|
+
}`;
|
|
131
|
+
const fallback = `${mapping}${address}`;
|
|
132
|
+
|
|
133
|
+
return fallback === '' ? '<unknown>' : fallback;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function IcicleGraphNodes({
|
|
137
|
+
data,
|
|
138
|
+
x,
|
|
139
|
+
y,
|
|
140
|
+
xScale,
|
|
141
|
+
total,
|
|
142
|
+
totalWidth,
|
|
143
|
+
level,
|
|
144
|
+
setHoveringNode,
|
|
145
|
+
path,
|
|
146
|
+
setCurPath,
|
|
147
|
+
curPath,
|
|
148
|
+
}: IcicleGraphNodesProps) {
|
|
149
|
+
const nodes =
|
|
150
|
+
curPath.length === 0 ? data : data.filter(d => d != null && curPath[0] === nodeLabel(d));
|
|
151
|
+
|
|
152
|
+
const nextLevel = level + 1;
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<g transform={`translate(${x}, ${y})`}>
|
|
156
|
+
{nodes.map((d, i) => {
|
|
157
|
+
const start = nodes.slice(0, i).reduce((sum, d) => sum + d.cumulative, 0);
|
|
158
|
+
|
|
159
|
+
const nextCurPath = curPath.length === 0 ? [] : curPath.slice(1);
|
|
160
|
+
const width =
|
|
161
|
+
nextCurPath.length > 0 || (nextCurPath.length === 0 && curPath.length === 1)
|
|
162
|
+
? totalWidth
|
|
163
|
+
: xScale(d.cumulative);
|
|
164
|
+
|
|
165
|
+
if (width <= 1) {
|
|
166
|
+
return <></>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const key = `${level}-${i}`;
|
|
170
|
+
const name = nodeLabel(d);
|
|
171
|
+
const nextPath = path.concat([name]);
|
|
172
|
+
|
|
173
|
+
const color = diffColor(d.diff === undefined ? 0 : d.diff, d.cumulative);
|
|
174
|
+
|
|
175
|
+
const onClick = () => {
|
|
176
|
+
setCurPath(nextPath);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const xStart = xScale(start);
|
|
180
|
+
const newXScale =
|
|
181
|
+
nextCurPath.length === 0 && curPath.length === 1
|
|
182
|
+
? scaleLinear().domain([0, d.cumulative]).range([0, totalWidth])
|
|
183
|
+
: xScale;
|
|
184
|
+
|
|
185
|
+
const onMouseEnter = () => setHoveringNode(d);
|
|
186
|
+
const onMouseLeave = () => setHoveringNode(undefined);
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<React.Fragment>
|
|
190
|
+
<IcicleRect
|
|
191
|
+
key={`rect-${key}`}
|
|
192
|
+
x={xStart}
|
|
193
|
+
y={0}
|
|
194
|
+
width={width}
|
|
195
|
+
height={RowHeight}
|
|
196
|
+
name={name}
|
|
197
|
+
color={color}
|
|
198
|
+
onClick={onClick}
|
|
199
|
+
onMouseEnter={onMouseEnter}
|
|
200
|
+
onMouseLeave={onMouseLeave}
|
|
201
|
+
curPath={curPath}
|
|
202
|
+
/>
|
|
203
|
+
{data !== undefined && data.length > 0 && (
|
|
204
|
+
<IcicleGraphNodes
|
|
205
|
+
key={`node-${key}`}
|
|
206
|
+
data={d.childrenList}
|
|
207
|
+
x={xStart}
|
|
208
|
+
y={RowHeight}
|
|
209
|
+
xScale={newXScale}
|
|
210
|
+
total={total}
|
|
211
|
+
totalWidth={totalWidth}
|
|
212
|
+
level={nextLevel}
|
|
213
|
+
setHoveringNode={setHoveringNode}
|
|
214
|
+
path={nextPath}
|
|
215
|
+
curPath={nextCurPath}
|
|
216
|
+
setCurPath={setCurPath}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
</React.Fragment>
|
|
220
|
+
);
|
|
221
|
+
})}
|
|
222
|
+
</g>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const MemoizedIcicleGraphNodes = React.memo(IcicleGraphNodes);
|
|
227
|
+
|
|
228
|
+
interface FlamegraphTooltipProps {
|
|
229
|
+
x: number;
|
|
230
|
+
y: number;
|
|
231
|
+
unit: string;
|
|
232
|
+
total: number;
|
|
233
|
+
hoveringNode: FlamegraphNode.AsObject | FlamegraphRootNode.AsObject | undefined;
|
|
234
|
+
contextElement: Element | null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const FlamegraphNodeTooltipTableRows = ({
|
|
238
|
+
hoveringNode,
|
|
239
|
+
}: {
|
|
240
|
+
hoveringNode: FlamegraphNode.AsObject;
|
|
241
|
+
}): JSX.Element => {
|
|
242
|
+
if (hoveringNode.meta === undefined) return <></>;
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<>
|
|
246
|
+
{hoveringNode.meta.pb_function?.filename !== undefined &&
|
|
247
|
+
hoveringNode.meta.pb_function?.filename !== '' && (
|
|
248
|
+
<tr>
|
|
249
|
+
<td className="w-1/5">File</td>
|
|
250
|
+
<td className="w-4/5">
|
|
251
|
+
{hoveringNode.meta.pb_function.filename}
|
|
252
|
+
{hoveringNode.meta.line?.line !== undefined && hoveringNode.meta.line?.line !== 0
|
|
253
|
+
? ` +${hoveringNode.meta.line.line.toString()}`
|
|
254
|
+
: `${
|
|
255
|
+
hoveringNode.meta.pb_function?.startLine !== undefined &&
|
|
256
|
+
hoveringNode.meta.pb_function?.startLine !== 0
|
|
257
|
+
? ` +${hoveringNode.meta.pb_function.startLine.toString()}`
|
|
258
|
+
: ''
|
|
259
|
+
}`}
|
|
260
|
+
</td>
|
|
261
|
+
</tr>
|
|
262
|
+
)}
|
|
263
|
+
{hoveringNode.meta.location?.address !== undefined &&
|
|
264
|
+
hoveringNode.meta.location?.address !== 0 && (
|
|
265
|
+
<tr>
|
|
266
|
+
<td className="w-1/5">Address</td>
|
|
267
|
+
<td className="w-4/5">{' 0x' + hoveringNode.meta.location.address.toString(16)}</td>
|
|
268
|
+
</tr>
|
|
269
|
+
)}
|
|
270
|
+
{hoveringNode.meta.mapping !== undefined && hoveringNode.meta.mapping.file !== '' && (
|
|
271
|
+
<tr>
|
|
272
|
+
<td className="w-1/5">Binary</td>
|
|
273
|
+
<td className="w-4/5">{getLastItem(hoveringNode.meta.mapping.file)}</td>
|
|
274
|
+
</tr>
|
|
275
|
+
)}
|
|
276
|
+
</>
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
function generateGetBoundingClientRect(contextElement: Element, x = 0, y = 0) {
|
|
281
|
+
const domRect = contextElement.getBoundingClientRect();
|
|
282
|
+
return () =>
|
|
283
|
+
({
|
|
284
|
+
width: 0,
|
|
285
|
+
height: 0,
|
|
286
|
+
top: domRect.y + y,
|
|
287
|
+
left: domRect.x + x,
|
|
288
|
+
right: domRect.x + x,
|
|
289
|
+
bottom: domRect.y + y,
|
|
290
|
+
} as ClientRect);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const virtualElement = {
|
|
294
|
+
getBoundingClientRect: () =>
|
|
295
|
+
({
|
|
296
|
+
width: 0,
|
|
297
|
+
height: 0,
|
|
298
|
+
top: 0,
|
|
299
|
+
left: 0,
|
|
300
|
+
right: 0,
|
|
301
|
+
bottom: 0,
|
|
302
|
+
} as ClientRect),
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export const FlamegraphTooltip = ({
|
|
306
|
+
x,
|
|
307
|
+
y,
|
|
308
|
+
unit,
|
|
309
|
+
total,
|
|
310
|
+
hoveringNode,
|
|
311
|
+
contextElement,
|
|
312
|
+
}: FlamegraphTooltipProps): JSX.Element => {
|
|
313
|
+
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
|
314
|
+
|
|
315
|
+
const {styles, attributes, ...popperProps} = usePopper(virtualElement, popperElement, {
|
|
316
|
+
placement: 'auto-start',
|
|
317
|
+
strategy: 'absolute',
|
|
318
|
+
modifiers: [
|
|
319
|
+
{
|
|
320
|
+
name: 'preventOverflow',
|
|
321
|
+
options: {
|
|
322
|
+
tether: false,
|
|
323
|
+
altAxis: true,
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'offset',
|
|
328
|
+
options: {
|
|
329
|
+
offset: [30, 30],
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const update = popperProps.update;
|
|
336
|
+
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
if (contextElement != null) {
|
|
339
|
+
virtualElement.getBoundingClientRect = generateGetBoundingClientRect(contextElement, x, y);
|
|
340
|
+
update?.();
|
|
341
|
+
}
|
|
342
|
+
}, [x, y, contextElement, update]);
|
|
343
|
+
|
|
344
|
+
if (hoveringNode === undefined || hoveringNode == null) return <></>;
|
|
345
|
+
|
|
346
|
+
const diff = hoveringNode.diff === undefined ? 0 : hoveringNode.diff;
|
|
347
|
+
const prevValue = hoveringNode.cumulative - diff;
|
|
348
|
+
const diffRatio = Math.abs(diff) > 0 ? diff / prevValue : 0;
|
|
349
|
+
const diffSign = diff > 0 ? '+' : '';
|
|
350
|
+
const diffValueText = diffSign + valueFormatter(diff, unit, 1);
|
|
351
|
+
const diffPercentageText = diffSign + (diffRatio * 100).toFixed(2) + '%';
|
|
352
|
+
const diffText = `${diffValueText} (${diffPercentageText})`;
|
|
353
|
+
|
|
354
|
+
const hoveringFlamegraphNode = hoveringNode as FlamegraphNode.AsObject;
|
|
355
|
+
const metaRows =
|
|
356
|
+
hoveringFlamegraphNode.meta === undefined ? (
|
|
357
|
+
<></>
|
|
358
|
+
) : (
|
|
359
|
+
<FlamegraphNodeTooltipTableRows hoveringNode={hoveringNode as FlamegraphNode.AsObject} />
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<div ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
|
364
|
+
<div className="flex">
|
|
365
|
+
<div className="m-auto">
|
|
366
|
+
<div
|
|
367
|
+
className="border-gray-300 dark:border-gray-500 bg-gray-50 dark:bg-gray-900 rounded-lg p-3 shadow-lg opacity-90"
|
|
368
|
+
style={{borderWidth: 1}}
|
|
369
|
+
>
|
|
370
|
+
<div className="flex flex-row">
|
|
371
|
+
<div className="ml-2 mr-6">
|
|
372
|
+
<span className="font-semibold">
|
|
373
|
+
{hoveringFlamegraphNode.meta === undefined ? (
|
|
374
|
+
<p>root</p>
|
|
375
|
+
) : (
|
|
376
|
+
<>
|
|
377
|
+
{hoveringFlamegraphNode.meta.pb_function !== undefined &&
|
|
378
|
+
hoveringFlamegraphNode.meta.pb_function.name !== '' ? (
|
|
379
|
+
<p>{hoveringFlamegraphNode.meta.pb_function.name}</p>
|
|
380
|
+
) : (
|
|
381
|
+
<>
|
|
382
|
+
{hoveringFlamegraphNode.meta.location !== undefined &&
|
|
383
|
+
hoveringFlamegraphNode.meta.location.address !== 0 ? (
|
|
384
|
+
<p>
|
|
385
|
+
{'0x' + hoveringFlamegraphNode.meta.location.address.toString(16)}
|
|
386
|
+
</p>
|
|
387
|
+
) : (
|
|
388
|
+
<p>unknown</p>
|
|
389
|
+
)}
|
|
390
|
+
</>
|
|
391
|
+
)}
|
|
392
|
+
</>
|
|
393
|
+
)}
|
|
394
|
+
</span>
|
|
395
|
+
<span className="text-gray-700 dark:text-gray-300 my-2">
|
|
396
|
+
<table className="table-fixed">
|
|
397
|
+
<tbody>
|
|
398
|
+
<tr>
|
|
399
|
+
<td className="w-1/5">Cumulative</td>
|
|
400
|
+
<td className="w-4/5">
|
|
401
|
+
{valueFormatter(hoveringNode.cumulative, unit, 2)} (
|
|
402
|
+
{((hoveringNode.cumulative * 100) / total).toFixed(2)}%)
|
|
403
|
+
</td>
|
|
404
|
+
</tr>
|
|
405
|
+
{hoveringNode.diff !== undefined && diff !== 0 && (
|
|
406
|
+
<tr>
|
|
407
|
+
<td className="w-1/5">Diff</td>
|
|
408
|
+
<td className="w-4/5">{diffText}</td>
|
|
409
|
+
</tr>
|
|
410
|
+
)}
|
|
411
|
+
{metaRows}
|
|
412
|
+
</tbody>
|
|
413
|
+
</table>
|
|
414
|
+
</span>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
interface IcicleGraphRootNodeProps {
|
|
425
|
+
node: FlamegraphRootNode.AsObject;
|
|
426
|
+
xScale: (value: number) => number;
|
|
427
|
+
total: number;
|
|
428
|
+
totalWidth: number;
|
|
429
|
+
curPath: string[];
|
|
430
|
+
setCurPath: (path: string[]) => void;
|
|
431
|
+
setHoveringNode: (
|
|
432
|
+
node: FlamegraphNode.AsObject | FlamegraphRootNode.AsObject | undefined
|
|
433
|
+
) => void;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function IcicleGraphRootNode({
|
|
437
|
+
node,
|
|
438
|
+
xScale,
|
|
439
|
+
total,
|
|
440
|
+
totalWidth,
|
|
441
|
+
setHoveringNode,
|
|
442
|
+
setCurPath,
|
|
443
|
+
curPath,
|
|
444
|
+
}: IcicleGraphRootNodeProps) {
|
|
445
|
+
const color = diffColor(node.diff === undefined ? 0 : node.diff, node.cumulative);
|
|
446
|
+
|
|
447
|
+
const onClick = () => setCurPath([]);
|
|
448
|
+
const onMouseEnter = () => setHoveringNode(node);
|
|
449
|
+
const onMouseLeave = () => setHoveringNode(undefined);
|
|
450
|
+
const path = [];
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
<g transform={'translate(0, 0)'}>
|
|
454
|
+
<IcicleRect
|
|
455
|
+
x={0}
|
|
456
|
+
y={0}
|
|
457
|
+
width={totalWidth}
|
|
458
|
+
height={RowHeight}
|
|
459
|
+
name={'root'}
|
|
460
|
+
color={color}
|
|
461
|
+
onClick={onClick}
|
|
462
|
+
onMouseEnter={onMouseEnter}
|
|
463
|
+
onMouseLeave={onMouseLeave}
|
|
464
|
+
curPath={curPath}
|
|
465
|
+
/>
|
|
466
|
+
<MemoizedIcicleGraphNodes
|
|
467
|
+
data={node.childrenList}
|
|
468
|
+
x={0}
|
|
469
|
+
y={RowHeight}
|
|
470
|
+
xScale={xScale}
|
|
471
|
+
total={total}
|
|
472
|
+
totalWidth={totalWidth}
|
|
473
|
+
level={0}
|
|
474
|
+
setHoveringNode={setHoveringNode}
|
|
475
|
+
path={path}
|
|
476
|
+
curPath={curPath}
|
|
477
|
+
setCurPath={setCurPath}
|
|
478
|
+
/>
|
|
479
|
+
</g>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const MemoizedIcicleGraphRootNode = React.memo(IcicleGraphRootNode);
|
|
484
|
+
|
|
485
|
+
interface IcicleGraphProps {
|
|
486
|
+
graph: Flamegraph.AsObject;
|
|
487
|
+
width?: number;
|
|
488
|
+
curPath: string[];
|
|
489
|
+
setCurPath: (path: string[]) => void;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export default function IcicleGraph({graph, width, setCurPath, curPath}: IcicleGraphProps) {
|
|
493
|
+
const [hoveringNode, setHoveringNode] = useState<
|
|
494
|
+
FlamegraphNode.AsObject | FlamegraphRootNode.AsObject | undefined
|
|
495
|
+
>();
|
|
496
|
+
const [pos, setPos] = useState([0, 0]);
|
|
497
|
+
const [height, setHeight] = useState(0);
|
|
498
|
+
const svg = useRef(null);
|
|
499
|
+
const ref = useRef<SVGGElement>(null);
|
|
500
|
+
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
if (ref.current != null) {
|
|
503
|
+
setHeight(ref?.current.getBoundingClientRect().height);
|
|
504
|
+
}
|
|
505
|
+
}, [width]);
|
|
506
|
+
|
|
507
|
+
if (graph.root === undefined || width === undefined) return <></>;
|
|
508
|
+
|
|
509
|
+
const throttledSetPos = throttle(setPos, 20);
|
|
510
|
+
const onMouseMove = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement>): void => {
|
|
511
|
+
// X/Y coordinate array relative to svg
|
|
512
|
+
const rel = pointer(e);
|
|
513
|
+
|
|
514
|
+
throttledSetPos([rel[0], rel[1]]);
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const xScale = scaleLinear().domain([0, graph.total]).range([0, width]);
|
|
518
|
+
|
|
519
|
+
return (
|
|
520
|
+
<div onMouseLeave={() => setHoveringNode(undefined)}>
|
|
521
|
+
<FlamegraphTooltip
|
|
522
|
+
unit={graph.unit}
|
|
523
|
+
total={graph.total}
|
|
524
|
+
x={pos[0]}
|
|
525
|
+
y={pos[1]}
|
|
526
|
+
hoveringNode={hoveringNode}
|
|
527
|
+
contextElement={svg.current}
|
|
528
|
+
/>
|
|
529
|
+
<svg width={width} height={height} onMouseMove={onMouseMove} ref={svg}>
|
|
530
|
+
<g ref={ref}>
|
|
531
|
+
<MemoizedIcicleGraphRootNode
|
|
532
|
+
node={graph.root}
|
|
533
|
+
setHoveringNode={setHoveringNode}
|
|
534
|
+
curPath={curPath}
|
|
535
|
+
setCurPath={setCurPath}
|
|
536
|
+
xScale={xScale}
|
|
537
|
+
total={graph.total}
|
|
538
|
+
totalWidth={width}
|
|
539
|
+
/>
|
|
540
|
+
</g>
|
|
541
|
+
</svg>
|
|
542
|
+
</div>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {action} from '@storybook/addon-actions';
|
|
2
|
+
import {Meta, Story, Canvas} from '@storybook/addon-docs';
|
|
3
|
+
import ProfileIcicleGraph from './ProfileIcicleGraph';
|
|
4
|
+
import {QueryResponse} from '@parca/client';
|
|
5
|
+
import queryResponseSimple from './testdata/fg-simple.json';
|
|
6
|
+
import queryResponseDiff from './testdata/fg-diff.json';
|
|
7
|
+
|
|
8
|
+
<Meta title="template/ProfileIcicleGraph" component={ProfileIcicleGraph} />
|
|
9
|
+
|
|
10
|
+
# ProfileIcicleGraph
|
|
11
|
+
|
|
12
|
+
ProfileIcicleGraph documentation.
|
|
13
|
+
|
|
14
|
+
<Canvas>
|
|
15
|
+
<Story
|
|
16
|
+
name="Simple"
|
|
17
|
+
args={{
|
|
18
|
+
graph: queryResponseSimple.flamegraph,
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
{args => <ProfileIcicleGraph {...args} />}
|
|
22
|
+
</Story>
|
|
23
|
+
<Story
|
|
24
|
+
name="Diff"
|
|
25
|
+
args={{
|
|
26
|
+
graph: queryResponseDiff.flamegraph,
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
{args => <ProfileIcicleGraph {...args} />}
|
|
30
|
+
</Story>
|
|
31
|
+
</Canvas>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import IcicleGraph from './IcicleGraph';
|
|
2
|
+
import {Flamegraph} from '@parca/client';
|
|
3
|
+
|
|
4
|
+
interface ProfileIcicleGraphProps {
|
|
5
|
+
width?: number;
|
|
6
|
+
graph: Flamegraph.AsObject | undefined;
|
|
7
|
+
curPath: string[] | [];
|
|
8
|
+
setNewCurPath: (path: string[]) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ProfileIcicleGraph = ({width, graph, curPath, setNewCurPath}: ProfileIcicleGraphProps) => {
|
|
12
|
+
if (graph === undefined) return <div>no data...</div>;
|
|
13
|
+
const total = graph.total;
|
|
14
|
+
if (total === 0) return <>Profile has no samples</>;
|
|
15
|
+
|
|
16
|
+
return <IcicleGraph width={width} graph={graph} curPath={curPath} setCurPath={setNewCurPath} />;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default ProfileIcicleGraph;
|