@parca/profile 0.12.20 → 0.12.26
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 +14 -0
- package/package.json +4 -4
- package/src/IcicleGraph.tsx +81 -57
- package/src/ProfileIcicleGraph.tsx +19 -4
- package/src/ProfileSource.tsx +96 -87
- package/src/ProfileView.tsx +66 -88
- package/src/TopTable.tsx +25 -58
- package/src/useQuery.tsx +42 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.12.26](https://github.com/parca-dev/parca/compare/ui-v0.12.25...ui-v0.12.26) (2022-04-28)
|
|
7
|
+
|
|
8
|
+
## [0.12.24](https://github.com/parca-dev/parca/compare/ui-v0.12.23...ui-v0.12.24) (2022-04-26)
|
|
9
|
+
|
|
10
|
+
**Note:** Version bump only for package @parca/profile
|
|
11
|
+
|
|
12
|
+
## [0.12.25](https://github.com/parca-dev/parca/compare/ui-v0.12.24...ui-v0.12.25) (2022-04-27)
|
|
13
|
+
|
|
14
|
+
**Note:** Version bump only for package @parca/profile
|
|
15
|
+
|
|
16
|
+
## [0.12.23](https://github.com/parca-dev/parca/compare/ui-v0.12.22...ui-v0.12.23) (2022-04-25)
|
|
17
|
+
|
|
18
|
+
**Note:** Version bump only for package @parca/profile
|
|
19
|
+
|
|
6
20
|
## [0.12.20](https://github.com/parca-dev/parca/compare/ui-v0.12.19...ui-v0.12.20) (2022-04-12)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package @parca/profile
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parca/profile",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.26",
|
|
4
4
|
"description": "Profile viewing libraries",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@parca/client": "^0.12.
|
|
6
|
+
"@parca/client": "^0.12.26",
|
|
7
7
|
"@parca/dynamicsize": "^0.12.0",
|
|
8
|
-
"@parca/parser": "^0.12.
|
|
8
|
+
"@parca/parser": "^0.12.26",
|
|
9
9
|
"d3-scale": "^4.0.2"
|
|
10
10
|
},
|
|
11
11
|
"main": "src/index.tsx",
|
|
@@ -19,5 +19,5 @@
|
|
|
19
19
|
"access": "public",
|
|
20
20
|
"registry": "https://registry.npmjs.org/"
|
|
21
21
|
},
|
|
22
|
-
"gitHead": "
|
|
22
|
+
"gitHead": "a5187e802f95bb7ee967121af9c9b74167d95278"
|
|
23
23
|
}
|
package/src/IcicleGraph.tsx
CHANGED
|
@@ -5,8 +5,9 @@ import {scaleLinear} from 'd3-scale';
|
|
|
5
5
|
import {Flamegraph, FlamegraphNode, FlamegraphRootNode} from '@parca/client';
|
|
6
6
|
import {usePopper} from 'react-popper';
|
|
7
7
|
import {getLastItem, valueFormatter} from '@parca/functions';
|
|
8
|
+
import {useAppSelector, selectDarkMode} from '@parca/store';
|
|
8
9
|
|
|
9
|
-
const RowHeight =
|
|
10
|
+
const RowHeight = 26;
|
|
10
11
|
|
|
11
12
|
const icicleRectStyles = {
|
|
12
13
|
cursor: 'pointer',
|
|
@@ -65,7 +66,7 @@ function IcicleRect({
|
|
|
65
66
|
/>
|
|
66
67
|
{width > 5 && (
|
|
67
68
|
<svg width={width - 5} height={height}>
|
|
68
|
-
<text x={5} y={
|
|
69
|
+
<text x={5} y={15} style={{fontSize: '12px'}}>
|
|
69
70
|
{name}
|
|
70
71
|
</text>
|
|
71
72
|
</svg>
|
|
@@ -75,7 +76,7 @@ function IcicleRect({
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
interface IcicleGraphNodesProps {
|
|
78
|
-
data: FlamegraphNode
|
|
79
|
+
data: FlamegraphNode[];
|
|
79
80
|
x: number;
|
|
80
81
|
y: number;
|
|
81
82
|
total: number;
|
|
@@ -83,38 +84,20 @@ interface IcicleGraphNodesProps {
|
|
|
83
84
|
level: number;
|
|
84
85
|
curPath: string[];
|
|
85
86
|
setCurPath: (path: string[]) => void;
|
|
86
|
-
setHoveringNode: (
|
|
87
|
-
node: FlamegraphNode.AsObject | FlamegraphRootNode.AsObject | undefined
|
|
88
|
-
) => void;
|
|
87
|
+
setHoveringNode: (node: FlamegraphNode | FlamegraphRootNode | undefined) => void;
|
|
89
88
|
path: string[];
|
|
90
89
|
xScale: (value: number) => number;
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
function
|
|
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
|
-
export function nodeLabel(node: FlamegraphNode.AsObject): string {
|
|
92
|
+
export function nodeLabel(node: FlamegraphNode): string {
|
|
110
93
|
if (node.meta === undefined) return '<unknown>';
|
|
111
94
|
const mapping = `${
|
|
112
95
|
node.meta?.mapping?.file !== undefined && node.meta?.mapping?.file !== ''
|
|
113
96
|
? '[' + getLastItem(node.meta.mapping.file) + '] '
|
|
114
97
|
: ''
|
|
115
98
|
}`;
|
|
116
|
-
if (node.meta.
|
|
117
|
-
return mapping + node.meta.
|
|
99
|
+
if (node.meta.function?.name !== undefined && node.meta.function?.name !== '')
|
|
100
|
+
return mapping + node.meta.function.name;
|
|
118
101
|
|
|
119
102
|
const address = `${
|
|
120
103
|
node.meta.location?.address !== undefined && node.meta.location?.address !== 0
|
|
@@ -126,6 +109,26 @@ export function nodeLabel(node: FlamegraphNode.AsObject): string {
|
|
|
126
109
|
return fallback === '' ? '<unknown>' : fallback;
|
|
127
110
|
}
|
|
128
111
|
|
|
112
|
+
function diffColor(diff: number, cumulative: number, isDarkMode: boolean): string {
|
|
113
|
+
const prevValue = cumulative - diff;
|
|
114
|
+
const diffRatio = prevValue > 0 ? (Math.abs(diff) > 0 ? diff / prevValue : 0) : 1.0;
|
|
115
|
+
|
|
116
|
+
const diffTransparency =
|
|
117
|
+
Math.abs(diff) > 0 ? Math.min((Math.abs(diffRatio) / 2 + 0.5) * 0.8, 0.8) : 0;
|
|
118
|
+
|
|
119
|
+
const newSpanColor = isDarkMode ? '#B3BAE1' : '#929FEB';
|
|
120
|
+
const increasedSpanColor = isDarkMode
|
|
121
|
+
? `rgba(255, 177, 204, ${diffTransparency})`
|
|
122
|
+
: `rgba(254, 153, 187, ${diffTransparency})`;
|
|
123
|
+
const reducedSpanColor = isDarkMode
|
|
124
|
+
? `rgba(103, 158, 92, ${diffTransparency})`
|
|
125
|
+
: `rgba(164, 214, 153, ${diffTransparency})`;
|
|
126
|
+
|
|
127
|
+
const color = diff === 0 ? newSpanColor : diff > 0 ? increasedSpanColor : reducedSpanColor;
|
|
128
|
+
|
|
129
|
+
return color;
|
|
130
|
+
}
|
|
131
|
+
|
|
129
132
|
export function IcicleGraphNodes({
|
|
130
133
|
data,
|
|
131
134
|
x,
|
|
@@ -139,6 +142,8 @@ export function IcicleGraphNodes({
|
|
|
139
142
|
setCurPath,
|
|
140
143
|
curPath,
|
|
141
144
|
}: IcicleGraphNodesProps) {
|
|
145
|
+
const isDarkMode = useAppSelector(selectDarkMode);
|
|
146
|
+
|
|
142
147
|
const nodes =
|
|
143
148
|
curPath.length === 0 ? data : data.filter(d => d != null && curPath[0] === nodeLabel(d));
|
|
144
149
|
|
|
@@ -147,23 +152,25 @@ export function IcicleGraphNodes({
|
|
|
147
152
|
return (
|
|
148
153
|
<g transform={`translate(${x}, ${y})`}>
|
|
149
154
|
{nodes.map((d, i) => {
|
|
150
|
-
const
|
|
155
|
+
const cumulative = parseFloat(d.cumulative);
|
|
156
|
+
const diff = parseFloat(d.diff);
|
|
157
|
+
const start = nodes.slice(0, i).reduce((sum, d) => sum + parseFloat(d.cumulative), 0);
|
|
151
158
|
|
|
152
159
|
const nextCurPath = curPath.length === 0 ? [] : curPath.slice(1);
|
|
153
160
|
const width =
|
|
154
161
|
nextCurPath.length > 0 || (nextCurPath.length === 0 && curPath.length === 1)
|
|
155
162
|
? totalWidth
|
|
156
|
-
: xScale(
|
|
163
|
+
: xScale(cumulative);
|
|
157
164
|
|
|
158
165
|
if (width <= 1) {
|
|
159
166
|
return <></>;
|
|
160
167
|
}
|
|
161
168
|
|
|
162
|
-
const key = `${level}-${i}`;
|
|
163
169
|
const name = nodeLabel(d);
|
|
170
|
+
const key = `${level}-${i}`;
|
|
164
171
|
const nextPath = path.concat([name]);
|
|
165
172
|
|
|
166
|
-
const color = diffColor(
|
|
173
|
+
const color = diffColor(diff, cumulative, isDarkMode);
|
|
167
174
|
|
|
168
175
|
const onClick = () => {
|
|
169
176
|
setCurPath(nextPath);
|
|
@@ -172,7 +179,7 @@ export function IcicleGraphNodes({
|
|
|
172
179
|
const xStart = xScale(start);
|
|
173
180
|
const newXScale =
|
|
174
181
|
nextCurPath.length === 0 && curPath.length === 1
|
|
175
|
-
? scaleLinear().domain([0,
|
|
182
|
+
? scaleLinear().domain([0, cumulative]).range([0, totalWidth])
|
|
176
183
|
: xScale;
|
|
177
184
|
|
|
178
185
|
const onMouseEnter = () => setHoveringNode(d);
|
|
@@ -196,7 +203,7 @@ export function IcicleGraphNodes({
|
|
|
196
203
|
{data !== undefined && data.length > 0 && (
|
|
197
204
|
<IcicleGraphNodes
|
|
198
205
|
key={`node-${key}`}
|
|
199
|
-
data={d.
|
|
206
|
+
data={d.children}
|
|
200
207
|
x={xStart}
|
|
201
208
|
y={RowHeight}
|
|
202
209
|
xScale={newXScale}
|
|
@@ -223,14 +230,14 @@ interface FlamegraphTooltipProps {
|
|
|
223
230
|
y: number;
|
|
224
231
|
unit: string;
|
|
225
232
|
total: number;
|
|
226
|
-
hoveringNode: FlamegraphNode
|
|
233
|
+
hoveringNode: FlamegraphNode | FlamegraphRootNode | undefined;
|
|
227
234
|
contextElement: Element | null;
|
|
228
235
|
}
|
|
229
236
|
|
|
230
237
|
const FlamegraphNodeTooltipTableRows = ({
|
|
231
238
|
hoveringNode,
|
|
232
239
|
}: {
|
|
233
|
-
hoveringNode: FlamegraphNode
|
|
240
|
+
hoveringNode: FlamegraphNode;
|
|
234
241
|
}): JSX.Element => {
|
|
235
242
|
if (hoveringNode.meta === undefined) return <></>;
|
|
236
243
|
|
|
@@ -338,20 +345,21 @@ export const FlamegraphTooltip = ({
|
|
|
338
345
|
|
|
339
346
|
if (hoveringNode === undefined || hoveringNode == null) return <></>;
|
|
340
347
|
|
|
341
|
-
const
|
|
342
|
-
const
|
|
348
|
+
const hoveringNodeCumulative = parseFloat(hoveringNode.cumulative);
|
|
349
|
+
const diff = hoveringNode.diff === undefined ? 0 : parseFloat(hoveringNode.diff);
|
|
350
|
+
const prevValue = hoveringNodeCumulative - diff;
|
|
343
351
|
const diffRatio = Math.abs(diff) > 0 ? diff / prevValue : 0;
|
|
344
352
|
const diffSign = diff > 0 ? '+' : '';
|
|
345
353
|
const diffValueText = diffSign + valueFormatter(diff, unit, 1);
|
|
346
354
|
const diffPercentageText = diffSign + (diffRatio * 100).toFixed(2) + '%';
|
|
347
355
|
const diffText = `${diffValueText} (${diffPercentageText})`;
|
|
348
356
|
|
|
349
|
-
const hoveringFlamegraphNode = hoveringNode as FlamegraphNode
|
|
357
|
+
const hoveringFlamegraphNode = hoveringNode as FlamegraphNode;
|
|
350
358
|
const metaRows =
|
|
351
359
|
hoveringFlamegraphNode.meta === undefined ? (
|
|
352
360
|
<></>
|
|
353
361
|
) : (
|
|
354
|
-
<FlamegraphNodeTooltipTableRows hoveringNode={hoveringNode as FlamegraphNode
|
|
362
|
+
<FlamegraphNodeTooltipTableRows hoveringNode={hoveringNode as FlamegraphNode} />
|
|
355
363
|
);
|
|
356
364
|
|
|
357
365
|
return (
|
|
@@ -369,13 +377,13 @@ export const FlamegraphTooltip = ({
|
|
|
369
377
|
<p>root</p>
|
|
370
378
|
) : (
|
|
371
379
|
<>
|
|
372
|
-
{hoveringFlamegraphNode.meta.
|
|
373
|
-
hoveringFlamegraphNode.meta.
|
|
374
|
-
<p>{hoveringFlamegraphNode.meta.
|
|
380
|
+
{hoveringFlamegraphNode.meta.function !== undefined &&
|
|
381
|
+
hoveringFlamegraphNode.meta.function.name !== '' ? (
|
|
382
|
+
<p>{hoveringFlamegraphNode.meta.function.name}</p>
|
|
375
383
|
) : (
|
|
376
384
|
<>
|
|
377
385
|
{hoveringFlamegraphNode.meta.location !== undefined &&
|
|
378
|
-
hoveringFlamegraphNode.meta.location.address !== 0 ? (
|
|
386
|
+
parseInt(hoveringFlamegraphNode.meta.location.address, 10) !== 0 ? (
|
|
379
387
|
<p>
|
|
380
388
|
{'0x' + hoveringFlamegraphNode.meta.location.address.toString(16)}
|
|
381
389
|
</p>
|
|
@@ -393,8 +401,8 @@ export const FlamegraphTooltip = ({
|
|
|
393
401
|
<tr>
|
|
394
402
|
<td className="w-1/5">Cumulative</td>
|
|
395
403
|
<td className="w-4/5">
|
|
396
|
-
{valueFormatter(
|
|
397
|
-
{((
|
|
404
|
+
{valueFormatter(hoveringNodeCumulative, unit, 2)} (
|
|
405
|
+
{((hoveringNodeCumulative * 100) / total).toFixed(2)}%)
|
|
398
406
|
</td>
|
|
399
407
|
</tr>
|
|
400
408
|
{hoveringNode.diff !== undefined && diff !== 0 && (
|
|
@@ -417,15 +425,13 @@ export const FlamegraphTooltip = ({
|
|
|
417
425
|
};
|
|
418
426
|
|
|
419
427
|
interface IcicleGraphRootNodeProps {
|
|
420
|
-
node: FlamegraphRootNode
|
|
428
|
+
node: FlamegraphRootNode;
|
|
421
429
|
xScale: (value: number) => number;
|
|
422
430
|
total: number;
|
|
423
431
|
totalWidth: number;
|
|
424
432
|
curPath: string[];
|
|
425
433
|
setCurPath: (path: string[]) => void;
|
|
426
|
-
setHoveringNode: (
|
|
427
|
-
node: FlamegraphNode.AsObject | FlamegraphRootNode.AsObject | undefined
|
|
428
|
-
) => void;
|
|
434
|
+
setHoveringNode: (node: FlamegraphNode | FlamegraphRootNode | undefined) => void;
|
|
429
435
|
}
|
|
430
436
|
|
|
431
437
|
export function IcicleGraphRootNode({
|
|
@@ -437,7 +443,11 @@ export function IcicleGraphRootNode({
|
|
|
437
443
|
setCurPath,
|
|
438
444
|
curPath,
|
|
439
445
|
}: IcicleGraphRootNodeProps) {
|
|
440
|
-
const
|
|
446
|
+
const isDarkMode = useAppSelector(selectDarkMode);
|
|
447
|
+
|
|
448
|
+
const cumulative = parseFloat(node.cumulative);
|
|
449
|
+
const diff = parseFloat(node.diff);
|
|
450
|
+
const color = diffColor(diff, cumulative, isDarkMode);
|
|
441
451
|
|
|
442
452
|
const onClick = () => setCurPath([]);
|
|
443
453
|
const onMouseEnter = () => setHoveringNode(node);
|
|
@@ -459,7 +469,7 @@ export function IcicleGraphRootNode({
|
|
|
459
469
|
curPath={curPath}
|
|
460
470
|
/>
|
|
461
471
|
<MemoizedIcicleGraphNodes
|
|
462
|
-
data={node.
|
|
472
|
+
data={node.children}
|
|
463
473
|
x={0}
|
|
464
474
|
y={RowHeight}
|
|
465
475
|
xScale={xScale}
|
|
@@ -478,15 +488,22 @@ export function IcicleGraphRootNode({
|
|
|
478
488
|
const MemoizedIcicleGraphRootNode = React.memo(IcicleGraphRootNode);
|
|
479
489
|
|
|
480
490
|
interface IcicleGraphProps {
|
|
481
|
-
graph: Flamegraph
|
|
491
|
+
graph: Flamegraph;
|
|
492
|
+
sampleUnit: string;
|
|
482
493
|
width?: number;
|
|
483
494
|
curPath: string[];
|
|
484
495
|
setCurPath: (path: string[]) => void;
|
|
485
496
|
}
|
|
486
497
|
|
|
487
|
-
export default function IcicleGraph({
|
|
498
|
+
export default function IcicleGraph({
|
|
499
|
+
graph,
|
|
500
|
+
width,
|
|
501
|
+
setCurPath,
|
|
502
|
+
curPath,
|
|
503
|
+
sampleUnit,
|
|
504
|
+
}: IcicleGraphProps) {
|
|
488
505
|
const [hoveringNode, setHoveringNode] = useState<
|
|
489
|
-
FlamegraphNode
|
|
506
|
+
FlamegraphNode | FlamegraphRootNode | undefined
|
|
490
507
|
>();
|
|
491
508
|
const [pos, setPos] = useState([0, 0]);
|
|
492
509
|
const [height, setHeight] = useState(0);
|
|
@@ -509,19 +526,26 @@ export default function IcicleGraph({graph, width, setCurPath, curPath}: IcicleG
|
|
|
509
526
|
throttledSetPos([rel[0], rel[1]]);
|
|
510
527
|
};
|
|
511
528
|
|
|
512
|
-
const
|
|
529
|
+
const total = parseFloat(graph.total);
|
|
530
|
+
const xScale = scaleLinear().domain([0, total]).range([0, width]);
|
|
513
531
|
|
|
514
532
|
return (
|
|
515
533
|
<div onMouseLeave={() => setHoveringNode(undefined)}>
|
|
516
534
|
<FlamegraphTooltip
|
|
517
|
-
unit={
|
|
518
|
-
total={
|
|
535
|
+
unit={sampleUnit}
|
|
536
|
+
total={total}
|
|
519
537
|
x={pos[0]}
|
|
520
538
|
y={pos[1]}
|
|
521
539
|
hoveringNode={hoveringNode}
|
|
522
540
|
contextElement={svg.current}
|
|
523
541
|
/>
|
|
524
|
-
<svg
|
|
542
|
+
<svg
|
|
543
|
+
className="font-robotoMono"
|
|
544
|
+
width={width}
|
|
545
|
+
height={height}
|
|
546
|
+
onMouseMove={onMouseMove}
|
|
547
|
+
ref={svg}
|
|
548
|
+
>
|
|
525
549
|
<g ref={ref}>
|
|
526
550
|
<MemoizedIcicleGraphRootNode
|
|
527
551
|
node={graph.root}
|
|
@@ -529,7 +553,7 @@ export default function IcicleGraph({graph, width, setCurPath, curPath}: IcicleG
|
|
|
529
553
|
curPath={curPath}
|
|
530
554
|
setCurPath={setCurPath}
|
|
531
555
|
xScale={xScale}
|
|
532
|
-
total={
|
|
556
|
+
total={total}
|
|
533
557
|
totalWidth={width}
|
|
534
558
|
/>
|
|
535
559
|
</g>
|
|
@@ -3,17 +3,32 @@ import {Flamegraph} from '@parca/client';
|
|
|
3
3
|
|
|
4
4
|
interface ProfileIcicleGraphProps {
|
|
5
5
|
width?: number;
|
|
6
|
-
graph: Flamegraph
|
|
6
|
+
graph: Flamegraph | undefined;
|
|
7
|
+
sampleUnit: string;
|
|
7
8
|
curPath: string[] | [];
|
|
8
9
|
setNewCurPath: (path: string[]) => void;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const ProfileIcicleGraph = ({
|
|
12
|
+
const ProfileIcicleGraph = ({
|
|
13
|
+
width,
|
|
14
|
+
graph,
|
|
15
|
+
curPath,
|
|
16
|
+
setNewCurPath,
|
|
17
|
+
sampleUnit,
|
|
18
|
+
}: ProfileIcicleGraphProps) => {
|
|
12
19
|
if (graph === undefined) return <div>no data...</div>;
|
|
13
20
|
const total = graph.total;
|
|
14
|
-
if (total === 0) return <>Profile has no samples</>;
|
|
21
|
+
if (parseFloat(total) === 0) return <>Profile has no samples</>;
|
|
15
22
|
|
|
16
|
-
return
|
|
23
|
+
return (
|
|
24
|
+
<IcicleGraph
|
|
25
|
+
width={width}
|
|
26
|
+
graph={graph}
|
|
27
|
+
curPath={curPath}
|
|
28
|
+
setCurPath={setNewCurPath}
|
|
29
|
+
sampleUnit={sampleUnit}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
17
32
|
};
|
|
18
33
|
|
|
19
34
|
export default ProfileIcicleGraph;
|
package/src/ProfileSource.tsx
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import {formatDate} from '@parca/functions';
|
|
3
|
-
import {Query} from '@parca/parser';
|
|
3
|
+
import {Query, ProfileType} from '@parca/parser';
|
|
4
4
|
import {
|
|
5
5
|
Label,
|
|
6
6
|
QueryRequest,
|
|
7
|
+
QueryRequest_Mode,
|
|
8
|
+
QueryRequest_ReportType,
|
|
7
9
|
ProfileDiffSelection,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
DiffProfile,
|
|
10
|
+
ProfileDiffSelection_Mode,
|
|
11
|
+
Timestamp,
|
|
11
12
|
} from '@parca/client';
|
|
12
|
-
import {Timestamp} from 'google-protobuf/google/protobuf/timestamp_pb';
|
|
13
13
|
|
|
14
14
|
export interface ProfileSource {
|
|
15
15
|
QueryRequest: () => QueryRequest;
|
|
16
|
+
ProfileType: () => ProfileType;
|
|
16
17
|
DiffSelection: () => ProfileDiffSelection;
|
|
17
18
|
Describe: () => JSX.Element;
|
|
18
19
|
toString: () => string;
|
|
@@ -42,8 +43,8 @@ export function SuffixParams(params: {[key: string]: any}, suffix: string): {[ke
|
|
|
42
43
|
);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
export function ParseLabels(labels: string[]): Label
|
|
46
|
-
return labels.map(function (labelString): Label
|
|
46
|
+
export function ParseLabels(labels: string[]): Label[] {
|
|
47
|
+
return labels.map(function (labelString): Label {
|
|
47
48
|
const parts = labelString.split('=', 2);
|
|
48
49
|
return {name: parts[0], value: parts[1]};
|
|
49
50
|
});
|
|
@@ -55,6 +56,7 @@ export function ProfileSelectionFromParams(
|
|
|
55
56
|
to: string | undefined,
|
|
56
57
|
merge: string | undefined,
|
|
57
58
|
labels: string[] | undefined,
|
|
59
|
+
profileName: string | undefined,
|
|
58
60
|
time: string | undefined
|
|
59
61
|
): ProfileSelection | null {
|
|
60
62
|
if (
|
|
@@ -66,28 +68,30 @@ export function ProfileSelectionFromParams(
|
|
|
66
68
|
) {
|
|
67
69
|
return new MergedProfileSelection(parseInt(from), parseInt(to), expression);
|
|
68
70
|
}
|
|
69
|
-
if (labels !== undefined && time !== undefined) {
|
|
70
|
-
return new SingleProfileSelection(ParseLabels(labels), parseInt(time));
|
|
71
|
+
if (labels !== undefined && time !== undefined && profileName !== undefined) {
|
|
72
|
+
return new SingleProfileSelection(profileName, ParseLabels(labels), parseInt(time));
|
|
71
73
|
}
|
|
72
74
|
return null;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
export class SingleProfileSelection implements ProfileSelection {
|
|
76
|
-
|
|
78
|
+
profileName: string;
|
|
79
|
+
labels: Label[];
|
|
77
80
|
time: number;
|
|
78
81
|
|
|
79
|
-
constructor(labels: Label
|
|
82
|
+
constructor(profileName: string, labels: Label[], time: number) {
|
|
83
|
+
this.profileName = profileName;
|
|
80
84
|
this.labels = labels;
|
|
81
85
|
this.time = time;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
ProfileName(): string {
|
|
85
|
-
|
|
86
|
-
return label !== undefined ? label.value : '';
|
|
89
|
+
return this.profileName;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
HistoryParams(): {[key: string]: any} {
|
|
90
93
|
return {
|
|
94
|
+
profile_name: this.profileName,
|
|
91
95
|
labels: this.labels.map(label => `${label.name}=${label.value}`),
|
|
92
96
|
time: this.time,
|
|
93
97
|
};
|
|
@@ -98,7 +102,7 @@ export class SingleProfileSelection implements ProfileSelection {
|
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
ProfileSource(): ProfileSource {
|
|
101
|
-
return new SingleProfileSource(this.labels, this.time);
|
|
105
|
+
return new SingleProfileSource(this.profileName, this.labels, this.time);
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
|
|
@@ -136,50 +140,58 @@ export class MergedProfileSelection implements ProfileSelection {
|
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
export class SingleProfileSource implements ProfileSource {
|
|
139
|
-
|
|
143
|
+
profName: string;
|
|
144
|
+
labels: Label[];
|
|
140
145
|
time: number;
|
|
141
146
|
|
|
142
|
-
constructor(labels: Label
|
|
147
|
+
constructor(profileName: string, labels: Label[], time: number) {
|
|
148
|
+
this.profName = profileName;
|
|
143
149
|
this.labels = labels;
|
|
144
150
|
this.time = time;
|
|
145
151
|
}
|
|
146
152
|
|
|
147
153
|
query(): string {
|
|
148
|
-
const seriesQuery =
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
const seriesQuery =
|
|
155
|
+
this.profName +
|
|
156
|
+
this.labels.reduce(function (agg: string, label: Label) {
|
|
157
|
+
return agg + `${label.name}="${label.value}",`;
|
|
158
|
+
}, '{');
|
|
151
159
|
return seriesQuery + '}';
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
DiffSelection(): ProfileDiffSelection {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return sel;
|
|
163
|
+
return {
|
|
164
|
+
options: {
|
|
165
|
+
oneofKind: 'single',
|
|
166
|
+
single: {
|
|
167
|
+
time: Timestamp.fromDate(new Date(this.time)),
|
|
168
|
+
query: this.query(),
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
mode: ProfileDiffSelection_Mode.SINGLE_UNSPECIFIED,
|
|
172
|
+
};
|
|
166
173
|
}
|
|
167
174
|
|
|
168
175
|
QueryRequest(): QueryRequest {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
return {
|
|
177
|
+
options: {
|
|
178
|
+
oneofKind: 'single',
|
|
179
|
+
single: {
|
|
180
|
+
time: Timestamp.fromDate(new Date(this.time)),
|
|
181
|
+
query: this.query(),
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
reportType: QueryRequest_ReportType.FLAMEGRAPH_UNSPECIFIED,
|
|
185
|
+
mode: QueryRequest_Mode.SINGLE_UNSPECIFIED,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
ProfileType(): ProfileType {
|
|
190
|
+
return ProfileType.fromString(this.profName);
|
|
178
191
|
}
|
|
179
192
|
|
|
180
193
|
profileName(): string {
|
|
181
|
-
|
|
182
|
-
return label !== undefined ? label.value : '';
|
|
194
|
+
return this.profName;
|
|
183
195
|
}
|
|
184
196
|
|
|
185
197
|
Describe(): JSX.Element {
|
|
@@ -208,8 +220,8 @@ export class SingleProfileSource implements ProfileSource {
|
|
|
208
220
|
|
|
209
221
|
stringLabels(): string[] {
|
|
210
222
|
return this.labels
|
|
211
|
-
.filter((label: Label
|
|
212
|
-
.map((label: Label
|
|
223
|
+
.filter((label: Label) => label.name !== '__name__')
|
|
224
|
+
.map((label: Label) => `${label.name}=${label.value}`);
|
|
213
225
|
}
|
|
214
226
|
|
|
215
227
|
toString(): string {
|
|
@@ -229,19 +241,25 @@ export class ProfileDiffSource implements ProfileSource {
|
|
|
229
241
|
}
|
|
230
242
|
|
|
231
243
|
DiffSelection(): ProfileDiffSelection {
|
|
232
|
-
|
|
244
|
+
throw new Error('Method not implemented.');
|
|
233
245
|
}
|
|
234
246
|
|
|
235
247
|
QueryRequest(): QueryRequest {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
248
|
+
return {
|
|
249
|
+
options: {
|
|
250
|
+
oneofKind: 'diff',
|
|
251
|
+
diff: {
|
|
252
|
+
a: this.a.DiffSelection(),
|
|
253
|
+
b: this.b.DiffSelection(),
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
reportType: QueryRequest_ReportType.FLAMEGRAPH_UNSPECIFIED,
|
|
257
|
+
mode: QueryRequest_Mode.DIFF,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
242
260
|
|
|
243
|
-
|
|
244
|
-
return
|
|
261
|
+
ProfileType(): ProfileType {
|
|
262
|
+
return this.a.ProfileType();
|
|
245
263
|
}
|
|
246
264
|
|
|
247
265
|
Describe(): JSX.Element {
|
|
@@ -269,45 +287,36 @@ export class MergedProfileSource implements ProfileSource {
|
|
|
269
287
|
}
|
|
270
288
|
|
|
271
289
|
DiffSelection(): ProfileDiffSelection {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
mergeProfile.setEnd(endTs);
|
|
284
|
-
|
|
285
|
-
mergeProfile.setQuery(this.query);
|
|
286
|
-
|
|
287
|
-
sel.setMerge(mergeProfile);
|
|
288
|
-
|
|
289
|
-
return sel;
|
|
290
|
+
return {
|
|
291
|
+
options: {
|
|
292
|
+
oneofKind: 'merge',
|
|
293
|
+
merge: {
|
|
294
|
+
start: Timestamp.fromDate(new Date(this.from)),
|
|
295
|
+
end: Timestamp.fromDate(new Date(this.to)),
|
|
296
|
+
query: this.query,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
mode: ProfileDiffSelection_Mode.MERGE,
|
|
300
|
+
};
|
|
290
301
|
}
|
|
291
302
|
|
|
292
303
|
QueryRequest(): QueryRequest {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
mergeQueryRequest.setQuery(this.query);
|
|
307
|
-
|
|
308
|
-
req.setMerge(mergeQueryRequest);
|
|
304
|
+
return {
|
|
305
|
+
options: {
|
|
306
|
+
oneofKind: 'merge',
|
|
307
|
+
merge: {
|
|
308
|
+
start: Timestamp.fromDate(new Date(this.from)),
|
|
309
|
+
end: Timestamp.fromDate(new Date(this.to)),
|
|
310
|
+
query: this.query,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
reportType: QueryRequest_ReportType.FLAMEGRAPH_UNSPECIFIED,
|
|
314
|
+
mode: QueryRequest_Mode.MERGE,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
309
317
|
|
|
310
|
-
|
|
318
|
+
ProfileType(): ProfileType {
|
|
319
|
+
return ProfileType.fromString(Query.parse(this.query).profileName());
|
|
311
320
|
}
|
|
312
321
|
|
|
313
322
|
Describe(): JSX.Element {
|
package/src/ProfileView.tsx
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import React, {useEffect, useState} from 'react';
|
|
2
2
|
import {CalcWidth} from '@parca/dynamicsize';
|
|
3
3
|
import {parseParams} from '@parca/functions';
|
|
4
|
-
import {
|
|
4
|
+
import {QueryServiceClient, QueryResponse, QueryRequest_ReportType} from '@parca/client';
|
|
5
|
+
import {RpcError} from '@protobuf-ts/runtime-rpc';
|
|
5
6
|
import {Button, Card, useGrpcMetadata} from '@parca/components';
|
|
6
7
|
import * as parca_query_v1alpha1_query_pb from '@parca/client/src/parca/query/v1alpha1/query_pb';
|
|
7
8
|
|
|
8
9
|
import ProfileIcicleGraph from './ProfileIcicleGraph';
|
|
9
10
|
import {ProfileSource} from './ProfileSource';
|
|
11
|
+
import {useQuery} from './useQuery';
|
|
10
12
|
import TopTable from './TopTable';
|
|
11
13
|
|
|
12
14
|
import './ProfileView.styles.css';
|
|
@@ -20,12 +22,6 @@ interface ProfileViewProps {
|
|
|
20
22
|
compare?: boolean;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export interface IQueryResult {
|
|
24
|
-
isLoading: boolean;
|
|
25
|
-
response: QueryResponse | null;
|
|
26
|
-
error: ServiceError | null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
25
|
function arrayEquals(a, b): boolean {
|
|
30
26
|
return (
|
|
31
27
|
Array.isArray(a) &&
|
|
@@ -35,41 +31,6 @@ function arrayEquals(a, b): boolean {
|
|
|
35
31
|
);
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
export const useQuery = (
|
|
39
|
-
client: QueryServiceClient,
|
|
40
|
-
profileSource: ProfileSource
|
|
41
|
-
): IQueryResult => {
|
|
42
|
-
const [result, setResult] = useState<IQueryResult>({
|
|
43
|
-
isLoading: false,
|
|
44
|
-
response: null,
|
|
45
|
-
error: null,
|
|
46
|
-
});
|
|
47
|
-
const metadata = useGrpcMetadata();
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
setResult({
|
|
51
|
-
...result,
|
|
52
|
-
isLoading: true,
|
|
53
|
-
});
|
|
54
|
-
const req = profileSource.QueryRequest();
|
|
55
|
-
req.setReportType(QueryRequest.ReportType.REPORT_TYPE_FLAMEGRAPH_UNSPECIFIED);
|
|
56
|
-
|
|
57
|
-
client.query(
|
|
58
|
-
req,
|
|
59
|
-
metadata,
|
|
60
|
-
(error: ServiceError | null, responseMessage: QueryResponse | null) => {
|
|
61
|
-
setResult({
|
|
62
|
-
isLoading: false,
|
|
63
|
-
response: responseMessage,
|
|
64
|
-
error: error,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
);
|
|
68
|
-
}, [client, profileSource]);
|
|
69
|
-
|
|
70
|
-
return result;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
34
|
export const ProfileView = ({
|
|
74
35
|
queryClient,
|
|
75
36
|
profileSource,
|
|
@@ -79,7 +40,11 @@ export const ProfileView = ({
|
|
|
79
40
|
const currentViewFromURL = router.currentProfileView as string;
|
|
80
41
|
const [curPath, setCurPath] = useState<string[]>([]);
|
|
81
42
|
const [isLoaderVisible, setIsLoaderVisible] = useState<boolean>(false);
|
|
82
|
-
const {isLoading, response, error} = useQuery(
|
|
43
|
+
const {isLoading, response, error} = useQuery(
|
|
44
|
+
queryClient,
|
|
45
|
+
profileSource,
|
|
46
|
+
QueryRequest_ReportType.FLAMEGRAPH_UNSPECIFIED
|
|
47
|
+
);
|
|
83
48
|
const [currentView, setCurrentView] = useState<string | undefined>(currentViewFromURL);
|
|
84
49
|
const grpcMetadata = useGrpcMetadata();
|
|
85
50
|
|
|
@@ -94,7 +59,7 @@ export const ProfileView = ({
|
|
|
94
59
|
setIsLoaderVisible(false);
|
|
95
60
|
}
|
|
96
61
|
return () => clearTimeout(showLoaderTimeout);
|
|
97
|
-
}, [isLoading]);
|
|
62
|
+
}, [isLoading, isLoaderVisible]);
|
|
98
63
|
|
|
99
64
|
if (isLoaderVisible) {
|
|
100
65
|
return (
|
|
@@ -132,40 +97,35 @@ export const ProfileView = ({
|
|
|
132
97
|
);
|
|
133
98
|
}
|
|
134
99
|
|
|
135
|
-
if (error) {
|
|
100
|
+
if (error !== null) {
|
|
136
101
|
return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
|
|
137
102
|
}
|
|
138
103
|
|
|
139
104
|
const downloadPProf = (e: React.MouseEvent<HTMLElement>) => {
|
|
140
105
|
e.preventDefault();
|
|
141
106
|
|
|
142
|
-
const req =
|
|
143
|
-
|
|
107
|
+
const req = {
|
|
108
|
+
...profileSource.QueryRequest(),
|
|
109
|
+
reportType: QueryRequest_ReportType.PPROF,
|
|
110
|
+
};
|
|
144
111
|
|
|
145
|
-
queryClient
|
|
146
|
-
req,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
responseMessage: parca_query_v1alpha1_query_pb.QueryResponse | null
|
|
151
|
-
) => {
|
|
152
|
-
if (error != null) {
|
|
153
|
-
console.error('Error while querying', error);
|
|
112
|
+
queryClient
|
|
113
|
+
.query(req, grpcMetadata)
|
|
114
|
+
.response.then(response => {
|
|
115
|
+
if (response.report.oneofKind !== 'pprof') {
|
|
116
|
+
console.log('Expected pprof report, got:', response.report.oneofKind);
|
|
154
117
|
return;
|
|
155
118
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
);
|
|
119
|
+
const blob = new Blob([response.report.pprof], {type: 'application/octet-stream'});
|
|
120
|
+
|
|
121
|
+
const link = document.createElement('a');
|
|
122
|
+
link.href = window.URL.createObjectURL(blob);
|
|
123
|
+
link.download = 'profile.pb.gz';
|
|
124
|
+
link.click();
|
|
125
|
+
})
|
|
126
|
+
.catch(error => {
|
|
127
|
+
console.error('Error while querying', error);
|
|
128
|
+
});
|
|
169
129
|
};
|
|
170
130
|
|
|
171
131
|
const resetIcicleGraph = () => setCurPath([]);
|
|
@@ -177,13 +137,15 @@ export const ProfileView = ({
|
|
|
177
137
|
};
|
|
178
138
|
|
|
179
139
|
const switchProfileView = (view: string) => {
|
|
180
|
-
if (
|
|
140
|
+
if (navigateTo === undefined) return;
|
|
181
141
|
|
|
182
142
|
setCurrentView(view);
|
|
183
143
|
|
|
184
144
|
navigateTo('/', {...router, ...{currentProfileView: view}});
|
|
185
145
|
};
|
|
186
146
|
|
|
147
|
+
const sampleUnit = profileSource.ProfileType().sampleUnit;
|
|
148
|
+
|
|
187
149
|
return (
|
|
188
150
|
<>
|
|
189
151
|
<div className="py-3">
|
|
@@ -204,7 +166,7 @@ export const ProfileView = ({
|
|
|
204
166
|
color="neutral"
|
|
205
167
|
onClick={resetIcicleGraph}
|
|
206
168
|
disabled={curPath.length === 0}
|
|
207
|
-
|
|
169
|
+
className="whitespace-nowrap text-ellipsis"
|
|
208
170
|
>
|
|
209
171
|
Reset View
|
|
210
172
|
</Button>
|
|
@@ -212,7 +174,7 @@ export const ProfileView = ({
|
|
|
212
174
|
|
|
213
175
|
<Button
|
|
214
176
|
color={`${currentView === 'table' ? 'primary' : 'neutral'}`}
|
|
215
|
-
|
|
177
|
+
className="rounded-tr-none rounded-br-none w-auto px-8 whitespace-nowrap text-ellipsis no-outline-on-buttons"
|
|
216
178
|
onClick={() => switchProfileView('table')}
|
|
217
179
|
>
|
|
218
180
|
Table
|
|
@@ -220,7 +182,7 @@ export const ProfileView = ({
|
|
|
220
182
|
|
|
221
183
|
<Button
|
|
222
184
|
color={`${currentView === 'both' ? 'primary' : 'neutral'}`}
|
|
223
|
-
|
|
185
|
+
className="rounded-tl-none rounded-tr-none rounded-bl-none rounded-br-none border-l-0 border-r-0 w-auto px-8 whitespace-nowrap no-outline-on-buttons no-outline-on-buttons text-ellipsis"
|
|
224
186
|
onClick={() => switchProfileView('both')}
|
|
225
187
|
>
|
|
226
188
|
Both
|
|
@@ -228,7 +190,7 @@ export const ProfileView = ({
|
|
|
228
190
|
|
|
229
191
|
<Button
|
|
230
192
|
color={`${currentView === 'icicle' ? 'primary' : 'neutral'}`}
|
|
231
|
-
|
|
193
|
+
className="rounded-tl-none rounded-bl-none w-auto px-8 whitespace-nowrap text-ellipsis no-outline-on-buttons"
|
|
232
194
|
onClick={() => switchProfileView('icicle')}
|
|
233
195
|
>
|
|
234
196
|
Icicle Graph
|
|
@@ -237,28 +199,39 @@ export const ProfileView = ({
|
|
|
237
199
|
</div>
|
|
238
200
|
|
|
239
201
|
<div className="flex space-x-4 justify-between">
|
|
240
|
-
{currentView === 'icicle' &&
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
202
|
+
{currentView === 'icicle' &&
|
|
203
|
+
response !== null &&
|
|
204
|
+
response.report.oneofKind === 'flamegraph' && (
|
|
205
|
+
<div className="w-full">
|
|
206
|
+
<CalcWidth throttle={300} delay={2000}>
|
|
207
|
+
<ProfileIcicleGraph
|
|
208
|
+
curPath={curPath}
|
|
209
|
+
setNewCurPath={setNewCurPath}
|
|
210
|
+
graph={response.report.flamegraph}
|
|
211
|
+
sampleUnit={sampleUnit}
|
|
212
|
+
/>
|
|
213
|
+
</CalcWidth>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
251
216
|
|
|
252
217
|
{currentView === 'table' && (
|
|
253
218
|
<div className="w-full">
|
|
254
|
-
<TopTable
|
|
219
|
+
<TopTable
|
|
220
|
+
queryClient={queryClient}
|
|
221
|
+
profileSource={profileSource}
|
|
222
|
+
sampleUnit={sampleUnit}
|
|
223
|
+
/>
|
|
255
224
|
</div>
|
|
256
225
|
)}
|
|
257
226
|
|
|
258
227
|
{currentView === 'both' && (
|
|
259
228
|
<>
|
|
260
229
|
<div className="w-1/2">
|
|
261
|
-
<TopTable
|
|
230
|
+
<TopTable
|
|
231
|
+
queryClient={queryClient}
|
|
232
|
+
profileSource={profileSource}
|
|
233
|
+
sampleUnit={sampleUnit}
|
|
234
|
+
/>
|
|
262
235
|
</div>
|
|
263
236
|
|
|
264
237
|
<div className="w-1/2">
|
|
@@ -266,7 +239,12 @@ export const ProfileView = ({
|
|
|
266
239
|
<ProfileIcicleGraph
|
|
267
240
|
curPath={curPath}
|
|
268
241
|
setNewCurPath={setNewCurPath}
|
|
269
|
-
graph={
|
|
242
|
+
graph={
|
|
243
|
+
response?.report.oneofKind === 'flamegraph'
|
|
244
|
+
? response.report.flamegraph
|
|
245
|
+
: undefined
|
|
246
|
+
}
|
|
247
|
+
sampleUnit={sampleUnit}
|
|
270
248
|
/>
|
|
271
249
|
</CalcWidth>
|
|
272
250
|
</div>
|
package/src/TopTable.tsx
CHANGED
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
import React, {useEffect, useState} from 'react';
|
|
2
|
+
import * as parca_query_v1alpha1_query_pb from '@parca/client/src/parca/query/v1alpha1/query_pb';
|
|
3
|
+
import {getLastItem, valueFormatter} from '@parca/functions';
|
|
4
|
+
import {useAppSelector, selectCompareMode} from '@parca/store';
|
|
2
5
|
import {
|
|
3
|
-
QueryRequest,
|
|
4
6
|
QueryResponse,
|
|
5
7
|
QueryServiceClient,
|
|
6
|
-
|
|
8
|
+
QueryRequest_ReportType,
|
|
7
9
|
TopNodeMeta,
|
|
8
10
|
} from '@parca/client';
|
|
9
|
-
import * as parca_query_v1alpha1_query_pb from '@parca/client/src/parca/query/v1alpha1/query_pb';
|
|
10
|
-
import {getLastItem, valueFormatter} from '@parca/functions';
|
|
11
|
-
import {useGrpcMetadata} from '@parca/components';
|
|
12
|
-
import {useAppSelector, selectCompareMode} from '@parca/store';
|
|
13
|
-
|
|
14
11
|
import {ProfileSource} from './ProfileSource';
|
|
12
|
+
import {useQuery} from './useQuery';
|
|
15
13
|
import './TopTable.styles.css';
|
|
16
14
|
|
|
17
15
|
interface ProfileViewProps {
|
|
18
16
|
queryClient: QueryServiceClient;
|
|
19
17
|
profileSource: ProfileSource;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export interface IQueryResult {
|
|
23
|
-
response: QueryResponse | null;
|
|
24
|
-
error: ServiceError | null;
|
|
18
|
+
sampleUnit: string;
|
|
25
19
|
}
|
|
26
20
|
|
|
27
21
|
const Arrow = ({direction}: {direction: string | undefined}) => {
|
|
@@ -47,11 +41,12 @@ const useSortableData = (
|
|
|
47
41
|
config
|
|
48
42
|
);
|
|
49
43
|
|
|
50
|
-
const rawTableReport =
|
|
44
|
+
const rawTableReport =
|
|
45
|
+
response !== null && response.report.oneofKind === 'top' ? response.report.top.list : [];
|
|
51
46
|
|
|
52
|
-
const items = rawTableReport
|
|
47
|
+
const items = rawTableReport.map(node => ({
|
|
53
48
|
...node,
|
|
54
|
-
name: node.meta?.
|
|
49
|
+
name: node.meta?.function?.name,
|
|
55
50
|
}));
|
|
56
51
|
|
|
57
52
|
const sortedItems = React.useMemo(() => {
|
|
@@ -83,71 +78,43 @@ const useSortableData = (
|
|
|
83
78
|
return {items: sortedItems, requestSort, sortConfig};
|
|
84
79
|
};
|
|
85
80
|
|
|
86
|
-
export const
|
|
87
|
-
client: QueryServiceClient,
|
|
88
|
-
profileSource: ProfileSource
|
|
89
|
-
): IQueryResult => {
|
|
90
|
-
const [result, setResult] = useState<IQueryResult>({
|
|
91
|
-
response: null,
|
|
92
|
-
error: null,
|
|
93
|
-
});
|
|
94
|
-
const metadata = useGrpcMetadata();
|
|
95
|
-
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
const req = profileSource.QueryRequest();
|
|
98
|
-
req.setReportType(QueryRequest.ReportType.REPORT_TYPE_TOP);
|
|
99
|
-
|
|
100
|
-
client.query(
|
|
101
|
-
req,
|
|
102
|
-
metadata,
|
|
103
|
-
(
|
|
104
|
-
error: ServiceError | null,
|
|
105
|
-
responseMessage: parca_query_v1alpha1_query_pb.QueryResponse | null
|
|
106
|
-
) => {
|
|
107
|
-
setResult({
|
|
108
|
-
response: responseMessage,
|
|
109
|
-
error: error,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
);
|
|
113
|
-
}, [client, profileSource]);
|
|
114
|
-
|
|
115
|
-
return result;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export const RowLabel = (meta: TopNodeMeta.AsObject | undefined): string => {
|
|
81
|
+
export const RowLabel = (meta: TopNodeMeta | undefined): string => {
|
|
119
82
|
if (meta === undefined) return '<unknown>';
|
|
120
83
|
const mapping = `${
|
|
121
84
|
meta?.mapping?.file !== undefined && meta?.mapping?.file !== ''
|
|
122
85
|
? `[${getLastItem(meta.mapping.file)}]`
|
|
123
86
|
: ''
|
|
124
87
|
}`;
|
|
125
|
-
if (meta.
|
|
126
|
-
return `${mapping} ${meta.
|
|
88
|
+
if (meta.function?.name !== undefined && meta.function?.name !== '')
|
|
89
|
+
return `${mapping} ${meta.function.name}`;
|
|
127
90
|
|
|
91
|
+
const addr = parseInt(meta.location?.address ?? '0', 10);
|
|
128
92
|
const address = `${
|
|
129
|
-
meta.location?.address !== undefined &&
|
|
130
|
-
? `0x${meta.location.address.toString(16)}`
|
|
131
|
-
: ''
|
|
93
|
+
meta.location?.address !== undefined && addr !== 0 ? `0x${addr.toString(16)}` : ''
|
|
132
94
|
}`;
|
|
133
95
|
const fallback = `${mapping} ${address}`;
|
|
134
96
|
|
|
135
97
|
return fallback === '' ? '<unknown>' : fallback;
|
|
136
98
|
};
|
|
137
99
|
|
|
138
|
-
export const TopTable = ({
|
|
139
|
-
|
|
100
|
+
export const TopTable = ({
|
|
101
|
+
queryClient,
|
|
102
|
+
profileSource,
|
|
103
|
+
sampleUnit,
|
|
104
|
+
}: ProfileViewProps): JSX.Element => {
|
|
105
|
+
const {response, error} = useQuery(queryClient, profileSource, QueryRequest_ReportType.TOP);
|
|
140
106
|
const {items, requestSort, sortConfig} = useSortableData(response);
|
|
141
107
|
|
|
142
108
|
const compareMode = useAppSelector(selectCompareMode);
|
|
143
109
|
|
|
144
|
-
const unit =
|
|
110
|
+
const unit = sampleUnit;
|
|
145
111
|
|
|
146
112
|
if (error != null) {
|
|
147
113
|
return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
|
|
148
114
|
}
|
|
149
115
|
|
|
150
|
-
const total =
|
|
116
|
+
const total =
|
|
117
|
+
response !== null && response.report.oneofKind === 'top' ? response.report.top.list.length : 0;
|
|
151
118
|
if (total === 0) return <>Profile has no samples</>;
|
|
152
119
|
|
|
153
120
|
const getClassNamesFor = name => {
|
|
@@ -167,7 +134,7 @@ export const TopTable = ({queryClient, profileSource}: ProfileViewProps): JSX.El
|
|
|
167
134
|
|
|
168
135
|
return (
|
|
169
136
|
<>
|
|
170
|
-
<div className="w-full">
|
|
137
|
+
<div className="w-full font-robotoMono">
|
|
171
138
|
<table className="iciclegraph-table table-fixed text-left w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
172
139
|
<thead className="bg-gray-50 dark:bg-gray-800">
|
|
173
140
|
<tr>
|
package/src/useQuery.tsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, {useEffect, useState} from 'react';
|
|
2
|
+
import {QueryServiceClient, QueryResponse, QueryRequest_ReportType} from '@parca/client';
|
|
3
|
+
import {RpcError} from '@protobuf-ts/runtime-rpc';
|
|
4
|
+
import {useGrpcMetadata} from '@parca/components';
|
|
5
|
+
import {ProfileSource} from './ProfileSource';
|
|
6
|
+
|
|
7
|
+
export interface IQueryResult {
|
|
8
|
+
response: QueryResponse | null;
|
|
9
|
+
error: RpcError | null;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const useQuery = (
|
|
14
|
+
client: QueryServiceClient,
|
|
15
|
+
profileSource: ProfileSource,
|
|
16
|
+
reportType: QueryRequest_ReportType
|
|
17
|
+
): IQueryResult => {
|
|
18
|
+
const [result, setResult] = useState<IQueryResult>({
|
|
19
|
+
response: null,
|
|
20
|
+
error: null,
|
|
21
|
+
isLoading: false,
|
|
22
|
+
});
|
|
23
|
+
const metadata = useGrpcMetadata();
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setResult({
|
|
27
|
+
response: null,
|
|
28
|
+
error: null,
|
|
29
|
+
isLoading: true,
|
|
30
|
+
});
|
|
31
|
+
const req = profileSource.QueryRequest();
|
|
32
|
+
req.reportType = reportType;
|
|
33
|
+
|
|
34
|
+
const call = client.query(req, metadata);
|
|
35
|
+
|
|
36
|
+
call.response
|
|
37
|
+
.then(response => setResult({response: response, error: null, isLoading: false}))
|
|
38
|
+
.catch(error => setResult({error: error, response: null, isLoading: false}));
|
|
39
|
+
}, [client, profileSource, metadata, reportType]);
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
};
|