@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.
@@ -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;
@@ -0,0 +1,3 @@
1
+ .ProfileSVG ::selection {
2
+ background: none;
3
+ }