@parca/profile 0.12.25 → 0.12.30
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 +47 -38
- package/src/ProfileIcicleGraph.tsx +19 -4
- package/src/ProfileSource.tsx +96 -87
- package/src/ProfileView.tsx +62 -84
- package/src/TopTable.tsx +24 -57
- 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.30](https://github.com/parca-dev/parca/compare/ui-v0.12.29...ui-v0.12.30) (2022-04-29)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @parca/profile
|
|
9
|
+
|
|
10
|
+
## [0.12.27](https://github.com/parca-dev/parca/compare/ui-v0.12.26...ui-v0.12.27) (2022-04-29)
|
|
11
|
+
|
|
12
|
+
**Note:** Version bump only for package @parca/profile
|
|
13
|
+
|
|
14
|
+
## [0.12.26](https://github.com/parca-dev/parca/compare/ui-v0.12.25...ui-v0.12.26) (2022-04-28)
|
|
15
|
+
|
|
16
|
+
## [0.12.24](https://github.com/parca-dev/parca/compare/ui-v0.12.23...ui-v0.12.24) (2022-04-26)
|
|
17
|
+
|
|
18
|
+
**Note:** Version bump only for package @parca/profile
|
|
19
|
+
|
|
6
20
|
## [0.12.25](https://github.com/parca-dev/parca/compare/ui-v0.12.24...ui-v0.12.25) (2022-04-27)
|
|
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.30",
|
|
4
4
|
"description": "Profile viewing libraries",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@parca/client": "^0.12.
|
|
6
|
+
"@parca/client": "^0.12.30",
|
|
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": "ba113635bf4708d3a730028d4c538a8ee1d03239"
|
|
23
23
|
}
|
package/src/IcicleGraph.tsx
CHANGED
|
@@ -76,7 +76,7 @@ function IcicleRect({
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
interface IcicleGraphNodesProps {
|
|
79
|
-
data: FlamegraphNode
|
|
79
|
+
data: FlamegraphNode[];
|
|
80
80
|
x: number;
|
|
81
81
|
y: number;
|
|
82
82
|
total: number;
|
|
@@ -84,22 +84,20 @@ interface IcicleGraphNodesProps {
|
|
|
84
84
|
level: number;
|
|
85
85
|
curPath: string[];
|
|
86
86
|
setCurPath: (path: string[]) => void;
|
|
87
|
-
setHoveringNode: (
|
|
88
|
-
node: FlamegraphNode.AsObject | FlamegraphRootNode.AsObject | undefined
|
|
89
|
-
) => void;
|
|
87
|
+
setHoveringNode: (node: FlamegraphNode | FlamegraphRootNode | undefined) => void;
|
|
90
88
|
path: string[];
|
|
91
89
|
xScale: (value: number) => number;
|
|
92
90
|
}
|
|
93
91
|
|
|
94
|
-
export function nodeLabel(node: FlamegraphNode
|
|
92
|
+
export function nodeLabel(node: FlamegraphNode): string {
|
|
95
93
|
if (node.meta === undefined) return '<unknown>';
|
|
96
94
|
const mapping = `${
|
|
97
95
|
node.meta?.mapping?.file !== undefined && node.meta?.mapping?.file !== ''
|
|
98
96
|
? '[' + getLastItem(node.meta.mapping.file) + '] '
|
|
99
97
|
: ''
|
|
100
98
|
}`;
|
|
101
|
-
if (node.meta.
|
|
102
|
-
return mapping + node.meta.
|
|
99
|
+
if (node.meta.function?.name !== undefined && node.meta.function?.name !== '')
|
|
100
|
+
return mapping + node.meta.function.name;
|
|
103
101
|
|
|
104
102
|
const address = `${
|
|
105
103
|
node.meta.location?.address !== undefined && node.meta.location?.address !== 0
|
|
@@ -154,23 +152,25 @@ export function IcicleGraphNodes({
|
|
|
154
152
|
return (
|
|
155
153
|
<g transform={`translate(${x}, ${y})`}>
|
|
156
154
|
{nodes.map((d, i) => {
|
|
157
|
-
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);
|
|
158
158
|
|
|
159
159
|
const nextCurPath = curPath.length === 0 ? [] : curPath.slice(1);
|
|
160
160
|
const width =
|
|
161
161
|
nextCurPath.length > 0 || (nextCurPath.length === 0 && curPath.length === 1)
|
|
162
162
|
? totalWidth
|
|
163
|
-
: xScale(
|
|
163
|
+
: xScale(cumulative);
|
|
164
164
|
|
|
165
165
|
if (width <= 1) {
|
|
166
166
|
return <></>;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
const key = `${level}-${i}`;
|
|
170
169
|
const name = nodeLabel(d);
|
|
170
|
+
const key = `${level}-${i}`;
|
|
171
171
|
const nextPath = path.concat([name]);
|
|
172
172
|
|
|
173
|
-
const color = diffColor(
|
|
173
|
+
const color = diffColor(diff, cumulative, isDarkMode);
|
|
174
174
|
|
|
175
175
|
const onClick = () => {
|
|
176
176
|
setCurPath(nextPath);
|
|
@@ -179,7 +179,7 @@ export function IcicleGraphNodes({
|
|
|
179
179
|
const xStart = xScale(start);
|
|
180
180
|
const newXScale =
|
|
181
181
|
nextCurPath.length === 0 && curPath.length === 1
|
|
182
|
-
? scaleLinear().domain([0,
|
|
182
|
+
? scaleLinear().domain([0, cumulative]).range([0, totalWidth])
|
|
183
183
|
: xScale;
|
|
184
184
|
|
|
185
185
|
const onMouseEnter = () => setHoveringNode(d);
|
|
@@ -203,7 +203,7 @@ export function IcicleGraphNodes({
|
|
|
203
203
|
{data !== undefined && data.length > 0 && (
|
|
204
204
|
<IcicleGraphNodes
|
|
205
205
|
key={`node-${key}`}
|
|
206
|
-
data={d.
|
|
206
|
+
data={d.children}
|
|
207
207
|
x={xStart}
|
|
208
208
|
y={RowHeight}
|
|
209
209
|
xScale={newXScale}
|
|
@@ -230,14 +230,14 @@ interface FlamegraphTooltipProps {
|
|
|
230
230
|
y: number;
|
|
231
231
|
unit: string;
|
|
232
232
|
total: number;
|
|
233
|
-
hoveringNode: FlamegraphNode
|
|
233
|
+
hoveringNode: FlamegraphNode | FlamegraphRootNode | undefined;
|
|
234
234
|
contextElement: Element | null;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
const FlamegraphNodeTooltipTableRows = ({
|
|
238
238
|
hoveringNode,
|
|
239
239
|
}: {
|
|
240
|
-
hoveringNode: FlamegraphNode
|
|
240
|
+
hoveringNode: FlamegraphNode;
|
|
241
241
|
}): JSX.Element => {
|
|
242
242
|
if (hoveringNode.meta === undefined) return <></>;
|
|
243
243
|
|
|
@@ -345,20 +345,21 @@ export const FlamegraphTooltip = ({
|
|
|
345
345
|
|
|
346
346
|
if (hoveringNode === undefined || hoveringNode == null) return <></>;
|
|
347
347
|
|
|
348
|
-
const
|
|
349
|
-
const
|
|
348
|
+
const hoveringNodeCumulative = parseFloat(hoveringNode.cumulative);
|
|
349
|
+
const diff = hoveringNode.diff === undefined ? 0 : parseFloat(hoveringNode.diff);
|
|
350
|
+
const prevValue = hoveringNodeCumulative - diff;
|
|
350
351
|
const diffRatio = Math.abs(diff) > 0 ? diff / prevValue : 0;
|
|
351
352
|
const diffSign = diff > 0 ? '+' : '';
|
|
352
353
|
const diffValueText = diffSign + valueFormatter(diff, unit, 1);
|
|
353
354
|
const diffPercentageText = diffSign + (diffRatio * 100).toFixed(2) + '%';
|
|
354
355
|
const diffText = `${diffValueText} (${diffPercentageText})`;
|
|
355
356
|
|
|
356
|
-
const hoveringFlamegraphNode = hoveringNode as FlamegraphNode
|
|
357
|
+
const hoveringFlamegraphNode = hoveringNode as FlamegraphNode;
|
|
357
358
|
const metaRows =
|
|
358
359
|
hoveringFlamegraphNode.meta === undefined ? (
|
|
359
360
|
<></>
|
|
360
361
|
) : (
|
|
361
|
-
<FlamegraphNodeTooltipTableRows hoveringNode={hoveringNode as FlamegraphNode
|
|
362
|
+
<FlamegraphNodeTooltipTableRows hoveringNode={hoveringNode as FlamegraphNode} />
|
|
362
363
|
);
|
|
363
364
|
|
|
364
365
|
return (
|
|
@@ -376,13 +377,13 @@ export const FlamegraphTooltip = ({
|
|
|
376
377
|
<p>root</p>
|
|
377
378
|
) : (
|
|
378
379
|
<>
|
|
379
|
-
{hoveringFlamegraphNode.meta.
|
|
380
|
-
hoveringFlamegraphNode.meta.
|
|
381
|
-
<p>{hoveringFlamegraphNode.meta.
|
|
380
|
+
{hoveringFlamegraphNode.meta.function !== undefined &&
|
|
381
|
+
hoveringFlamegraphNode.meta.function.name !== '' ? (
|
|
382
|
+
<p>{hoveringFlamegraphNode.meta.function.name}</p>
|
|
382
383
|
) : (
|
|
383
384
|
<>
|
|
384
385
|
{hoveringFlamegraphNode.meta.location !== undefined &&
|
|
385
|
-
hoveringFlamegraphNode.meta.location.address !== 0 ? (
|
|
386
|
+
parseInt(hoveringFlamegraphNode.meta.location.address, 10) !== 0 ? (
|
|
386
387
|
<p>
|
|
387
388
|
{'0x' + hoveringFlamegraphNode.meta.location.address.toString(16)}
|
|
388
389
|
</p>
|
|
@@ -400,8 +401,8 @@ export const FlamegraphTooltip = ({
|
|
|
400
401
|
<tr>
|
|
401
402
|
<td className="w-1/5">Cumulative</td>
|
|
402
403
|
<td className="w-4/5">
|
|
403
|
-
{valueFormatter(
|
|
404
|
-
{((
|
|
404
|
+
{valueFormatter(hoveringNodeCumulative, unit, 2)} (
|
|
405
|
+
{((hoveringNodeCumulative * 100) / total).toFixed(2)}%)
|
|
405
406
|
</td>
|
|
406
407
|
</tr>
|
|
407
408
|
{hoveringNode.diff !== undefined && diff !== 0 && (
|
|
@@ -424,15 +425,13 @@ export const FlamegraphTooltip = ({
|
|
|
424
425
|
};
|
|
425
426
|
|
|
426
427
|
interface IcicleGraphRootNodeProps {
|
|
427
|
-
node: FlamegraphRootNode
|
|
428
|
+
node: FlamegraphRootNode;
|
|
428
429
|
xScale: (value: number) => number;
|
|
429
430
|
total: number;
|
|
430
431
|
totalWidth: number;
|
|
431
432
|
curPath: string[];
|
|
432
433
|
setCurPath: (path: string[]) => void;
|
|
433
|
-
setHoveringNode: (
|
|
434
|
-
node: FlamegraphNode.AsObject | FlamegraphRootNode.AsObject | undefined
|
|
435
|
-
) => void;
|
|
434
|
+
setHoveringNode: (node: FlamegraphNode | FlamegraphRootNode | undefined) => void;
|
|
436
435
|
}
|
|
437
436
|
|
|
438
437
|
export function IcicleGraphRootNode({
|
|
@@ -446,7 +445,9 @@ export function IcicleGraphRootNode({
|
|
|
446
445
|
}: IcicleGraphRootNodeProps) {
|
|
447
446
|
const isDarkMode = useAppSelector(selectDarkMode);
|
|
448
447
|
|
|
449
|
-
const
|
|
448
|
+
const cumulative = parseFloat(node.cumulative);
|
|
449
|
+
const diff = parseFloat(node.diff);
|
|
450
|
+
const color = diffColor(diff, cumulative, isDarkMode);
|
|
450
451
|
|
|
451
452
|
const onClick = () => setCurPath([]);
|
|
452
453
|
const onMouseEnter = () => setHoveringNode(node);
|
|
@@ -468,7 +469,7 @@ export function IcicleGraphRootNode({
|
|
|
468
469
|
curPath={curPath}
|
|
469
470
|
/>
|
|
470
471
|
<MemoizedIcicleGraphNodes
|
|
471
|
-
data={node.
|
|
472
|
+
data={node.children}
|
|
472
473
|
x={0}
|
|
473
474
|
y={RowHeight}
|
|
474
475
|
xScale={xScale}
|
|
@@ -487,15 +488,22 @@ export function IcicleGraphRootNode({
|
|
|
487
488
|
const MemoizedIcicleGraphRootNode = React.memo(IcicleGraphRootNode);
|
|
488
489
|
|
|
489
490
|
interface IcicleGraphProps {
|
|
490
|
-
graph: Flamegraph
|
|
491
|
+
graph: Flamegraph;
|
|
492
|
+
sampleUnit: string;
|
|
491
493
|
width?: number;
|
|
492
494
|
curPath: string[];
|
|
493
495
|
setCurPath: (path: string[]) => void;
|
|
494
496
|
}
|
|
495
497
|
|
|
496
|
-
export default function IcicleGraph({
|
|
498
|
+
export default function IcicleGraph({
|
|
499
|
+
graph,
|
|
500
|
+
width,
|
|
501
|
+
setCurPath,
|
|
502
|
+
curPath,
|
|
503
|
+
sampleUnit,
|
|
504
|
+
}: IcicleGraphProps) {
|
|
497
505
|
const [hoveringNode, setHoveringNode] = useState<
|
|
498
|
-
FlamegraphNode
|
|
506
|
+
FlamegraphNode | FlamegraphRootNode | undefined
|
|
499
507
|
>();
|
|
500
508
|
const [pos, setPos] = useState([0, 0]);
|
|
501
509
|
const [height, setHeight] = useState(0);
|
|
@@ -518,13 +526,14 @@ export default function IcicleGraph({graph, width, setCurPath, curPath}: IcicleG
|
|
|
518
526
|
throttledSetPos([rel[0], rel[1]]);
|
|
519
527
|
};
|
|
520
528
|
|
|
521
|
-
const
|
|
529
|
+
const total = parseFloat(graph.total);
|
|
530
|
+
const xScale = scaleLinear().domain([0, total]).range([0, width]);
|
|
522
531
|
|
|
523
532
|
return (
|
|
524
533
|
<div onMouseLeave={() => setHoveringNode(undefined)}>
|
|
525
534
|
<FlamegraphTooltip
|
|
526
|
-
unit={
|
|
527
|
-
total={
|
|
535
|
+
unit={sampleUnit}
|
|
536
|
+
total={total}
|
|
528
537
|
x={pos[0]}
|
|
529
538
|
y={pos[1]}
|
|
530
539
|
hoveringNode={hoveringNode}
|
|
@@ -544,7 +553,7 @@ export default function IcicleGraph({graph, width, setCurPath, curPath}: IcicleG
|
|
|
544
553
|
curPath={curPath}
|
|
545
554
|
setCurPath={setCurPath}
|
|
546
555
|
xScale={xScale}
|
|
547
|
-
total={
|
|
556
|
+
total={total}
|
|
548
557
|
totalWidth={width}
|
|
549
558
|
/>
|
|
550
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">
|
|
@@ -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 => {
|
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
|
+
};
|