@parca/profile 0.16.136 → 0.16.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/Callgraph/index.d.ts +3 -3
- package/dist/Callgraph/index.js +79 -191
- package/dist/Callgraph/utils.js +15 -19
- package/dist/GraphTooltip/index.d.ts +22 -8
- package/dist/GraphTooltip/index.js +14 -14
- package/dist/ProfileIcicleGraph/IcicleGraph/IcicleGraphNodes.js +2 -1
- package/dist/ProfileView/index.js +113 -11
- package/dist/index.d.ts +1 -1
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/Callgraph/index.tsx +127 -323
- package/src/Callgraph/utils.ts +15 -21
- package/src/GraphTooltip/index.tsx +38 -13
- package/src/ProfileIcicleGraph/IcicleGraph/IcicleGraphNodes.tsx +2 -1
- package/src/ProfileView/index.tsx +72 -19
- package/typings.d.ts +1 -0
|
@@ -17,7 +17,13 @@ import {pointer} from 'd3-selection';
|
|
|
17
17
|
import {CopyToClipboard} from 'react-copy-to-clipboard';
|
|
18
18
|
import {usePopper} from 'react-popper';
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
CallgraphNode,
|
|
22
|
+
CallgraphNodeMeta,
|
|
23
|
+
FlamegraphNode,
|
|
24
|
+
FlamegraphNodeMeta,
|
|
25
|
+
FlamegraphRootNode,
|
|
26
|
+
} from '@parca/client';
|
|
21
27
|
import {
|
|
22
28
|
Location,
|
|
23
29
|
Mapping,
|
|
@@ -34,6 +40,16 @@ const NoData = (): JSX.Element => {
|
|
|
34
40
|
return <span className="rounded bg-gray-200 dark:bg-gray-800 px-2">Not available</span>;
|
|
35
41
|
};
|
|
36
42
|
|
|
43
|
+
interface ExtendedCallgraphNodeMeta extends CallgraphNodeMeta {
|
|
44
|
+
lineIndex: number;
|
|
45
|
+
locationIndex: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface HoveringNode extends FlamegraphRootNode, FlamegraphNode, CallgraphNode {
|
|
49
|
+
diff: string;
|
|
50
|
+
meta?: FlamegraphNodeMeta | ExtendedCallgraphNodeMeta;
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
interface GraphTooltipProps {
|
|
38
54
|
x?: number;
|
|
39
55
|
y?: number;
|
|
@@ -47,6 +63,7 @@ interface GraphTooltipProps {
|
|
|
47
63
|
mappings?: Mapping[];
|
|
48
64
|
locations?: Location[];
|
|
49
65
|
functions?: ParcaFunction[];
|
|
66
|
+
type?: string;
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
const virtualElement = {
|
|
@@ -83,16 +100,19 @@ const TooltipMetaInfo = ({
|
|
|
83
100
|
mappings,
|
|
84
101
|
locations,
|
|
85
102
|
functions,
|
|
103
|
+
type = 'flamegraph',
|
|
86
104
|
}: {
|
|
87
|
-
hoveringNode:
|
|
105
|
+
hoveringNode: HoveringNode;
|
|
88
106
|
onCopy: () => void;
|
|
89
107
|
strings?: string[];
|
|
90
108
|
mappings?: Mapping[];
|
|
91
109
|
locations?: Location[];
|
|
92
110
|
functions?: ParcaFunction[];
|
|
111
|
+
type?: string;
|
|
93
112
|
}): JSX.Element => {
|
|
94
113
|
// populate meta from the flamegraph metadata tables
|
|
95
114
|
if (
|
|
115
|
+
type === 'flamegraph' &&
|
|
96
116
|
locations !== undefined &&
|
|
97
117
|
hoveringNode.meta?.locationIndex !== undefined &&
|
|
98
118
|
hoveringNode.meta.locationIndex !== 0
|
|
@@ -133,7 +153,7 @@ const TooltipMetaInfo = ({
|
|
|
133
153
|
}
|
|
134
154
|
}
|
|
135
155
|
|
|
136
|
-
const getTextForFile = (hoveringNode:
|
|
156
|
+
const getTextForFile = (hoveringNode: HoveringNode): string => {
|
|
137
157
|
if (hoveringNode.meta?.function == null) return '<unknown>';
|
|
138
158
|
|
|
139
159
|
return `${hoveringNode.meta.function.filename} ${
|
|
@@ -218,15 +238,9 @@ const TooltipMetaInfo = ({
|
|
|
218
238
|
);
|
|
219
239
|
};
|
|
220
240
|
|
|
221
|
-
// @ts-expect-error
|
|
222
|
-
export interface HoveringNode extends CallgraphNode, FlamegraphRootNode, FlamegraphNode {
|
|
223
|
-
diff: string;
|
|
224
|
-
meta?: FlamegraphNodeMeta | {[key: string]: any};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
241
|
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
228
242
|
|
|
229
|
-
const GraphTooltipContent = ({
|
|
243
|
+
export const GraphTooltipContent = ({
|
|
230
244
|
hoveringNode,
|
|
231
245
|
unit,
|
|
232
246
|
total,
|
|
@@ -235,6 +249,7 @@ const GraphTooltipContent = ({
|
|
|
235
249
|
mappings,
|
|
236
250
|
locations,
|
|
237
251
|
functions,
|
|
252
|
+
type = 'flamegraph',
|
|
238
253
|
}: {
|
|
239
254
|
hoveringNode: HoveringNode;
|
|
240
255
|
unit: string;
|
|
@@ -244,6 +259,7 @@ const GraphTooltipContent = ({
|
|
|
244
259
|
mappings?: Mapping[];
|
|
245
260
|
locations?: Location[];
|
|
246
261
|
functions?: ParcaFunction[];
|
|
262
|
+
type?: string;
|
|
247
263
|
}): JSX.Element => {
|
|
248
264
|
const [isCopied, setIsCopied] = useState<boolean>(false);
|
|
249
265
|
|
|
@@ -336,12 +352,12 @@ const GraphTooltipContent = ({
|
|
|
336
352
|
)}
|
|
337
353
|
<TooltipMetaInfo
|
|
338
354
|
onCopy={onCopy}
|
|
339
|
-
// @ts-expect-error
|
|
340
355
|
hoveringNode={hoveringNode}
|
|
341
356
|
strings={strings}
|
|
342
357
|
mappings={mappings}
|
|
343
358
|
locations={locations}
|
|
344
359
|
functions={functions}
|
|
360
|
+
type={type}
|
|
345
361
|
/>
|
|
346
362
|
</tbody>
|
|
347
363
|
</table>
|
|
@@ -369,10 +385,12 @@ const GraphTooltip = ({
|
|
|
369
385
|
mappings,
|
|
370
386
|
locations,
|
|
371
387
|
functions,
|
|
388
|
+
type = 'flamegraph',
|
|
372
389
|
}: GraphTooltipProps): JSX.Element => {
|
|
373
390
|
const hoveringNodeState = useAppSelector(selectHoveringNode);
|
|
391
|
+
// @ts-expect-error
|
|
374
392
|
const hoveringNode = useMemo<HoveringNode>(() => {
|
|
375
|
-
const h =
|
|
393
|
+
const h = hoveringNodeProp ?? hoveringNodeState;
|
|
376
394
|
if (h == null) {
|
|
377
395
|
return h;
|
|
378
396
|
}
|
|
@@ -446,7 +464,13 @@ const GraphTooltip = ({
|
|
|
446
464
|
if (hoveringNode === undefined || hoveringNode == null) return <></>;
|
|
447
465
|
|
|
448
466
|
return isFixed ? (
|
|
449
|
-
<GraphTooltipContent
|
|
467
|
+
<GraphTooltipContent
|
|
468
|
+
hoveringNode={hoveringNode}
|
|
469
|
+
unit={unit}
|
|
470
|
+
total={total}
|
|
471
|
+
isFixed={isFixed}
|
|
472
|
+
type={type}
|
|
473
|
+
/>
|
|
450
474
|
) : (
|
|
451
475
|
<div ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
|
452
476
|
<GraphTooltipContent
|
|
@@ -458,6 +482,7 @@ const GraphTooltip = ({
|
|
|
458
482
|
mappings={mappings}
|
|
459
483
|
locations={locations}
|
|
460
484
|
functions={functions}
|
|
485
|
+
type={type}
|
|
461
486
|
/>
|
|
462
487
|
</div>
|
|
463
488
|
);
|
|
@@ -202,7 +202,8 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
|
|
|
202
202
|
const onMouseEnter = (): void => {
|
|
203
203
|
if (isShiftDown) return;
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
// need to add id and flat for tooltip purposes
|
|
206
|
+
dispatch(setHoveringNode({...data, id: '', flat: ''}));
|
|
206
207
|
};
|
|
207
208
|
const onMouseLeave = (): void => {
|
|
208
209
|
if (isShiftDown) return;
|
|
@@ -15,6 +15,7 @@ import {Profiler, ProfilerProps, useEffect, useMemo, useState} from 'react';
|
|
|
15
15
|
|
|
16
16
|
import cx from 'classnames';
|
|
17
17
|
import {scaleLinear} from 'd3';
|
|
18
|
+
import graphviz from 'graphviz-wasm';
|
|
18
19
|
import {
|
|
19
20
|
DragDropContext,
|
|
20
21
|
Draggable,
|
|
@@ -37,6 +38,7 @@ import {getNewSpanColor} from '@parca/functions';
|
|
|
37
38
|
import {selectDarkMode, useAppSelector} from '@parca/store';
|
|
38
39
|
|
|
39
40
|
import {Callgraph} from '../';
|
|
41
|
+
import {jsonToDot} from '../Callgraph/utils';
|
|
40
42
|
import ProfileIcicleGraph, {ResizeHandler} from '../ProfileIcicleGraph';
|
|
41
43
|
import {ProfileSource} from '../ProfileSource';
|
|
42
44
|
import {TopTable} from '../TopTable';
|
|
@@ -105,6 +107,8 @@ export const ProfileView = ({
|
|
|
105
107
|
param: 'dashboard_items',
|
|
106
108
|
navigateTo,
|
|
107
109
|
});
|
|
110
|
+
const [graphvizLoaded, setGraphvizLoaded] = useState(false);
|
|
111
|
+
const [callgraphSVG, setCallgraphSVG] = useState<string | undefined>(undefined);
|
|
108
112
|
const [currentSearchString] = useURLState({param: 'search_string'});
|
|
109
113
|
|
|
110
114
|
const dashboardItems = useMemo(() => {
|
|
@@ -124,22 +128,73 @@ export const ProfileView = ({
|
|
|
124
128
|
setCurPath([]);
|
|
125
129
|
}, [profileSource]);
|
|
126
130
|
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
async function loadGraphviz(): Promise<void> {
|
|
133
|
+
await graphviz.loadWASM();
|
|
134
|
+
setGraphvizLoaded(true);
|
|
135
|
+
}
|
|
136
|
+
void loadGraphviz();
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
127
139
|
const isLoading = useMemo(() => {
|
|
128
140
|
if (dashboardItems.includes('icicle')) {
|
|
129
141
|
return Boolean(flamegraphData?.loading);
|
|
130
142
|
}
|
|
131
143
|
if (dashboardItems.includes('callgraph')) {
|
|
132
|
-
return Boolean(callgraphData?.loading);
|
|
144
|
+
return Boolean(callgraphData?.loading) || Boolean(callgraphSVG === undefined);
|
|
133
145
|
}
|
|
134
146
|
if (dashboardItems.includes('table')) {
|
|
135
147
|
return Boolean(topTableData?.loading);
|
|
136
148
|
}
|
|
137
149
|
return false;
|
|
138
|
-
}, [
|
|
150
|
+
}, [
|
|
151
|
+
dashboardItems,
|
|
152
|
+
callgraphData?.loading,
|
|
153
|
+
flamegraphData?.loading,
|
|
154
|
+
topTableData?.loading,
|
|
155
|
+
callgraphSVG,
|
|
156
|
+
]);
|
|
139
157
|
|
|
140
158
|
const isLoaderVisible = useDelayedLoader(isLoading);
|
|
141
159
|
|
|
142
|
-
|
|
160
|
+
const maxColor: string = getNewSpanColor(isDarkMode);
|
|
161
|
+
const minColor: string = scaleLinear([isDarkMode ? 'black' : 'white', maxColor])(0.3);
|
|
162
|
+
const colorRange: [string, string] = [minColor, maxColor];
|
|
163
|
+
// Note: If we want to further optimize the experience, we could try to load the graphviz layout in the ProfileViewWithData layer
|
|
164
|
+
// and pass it down to the ProfileView. This would allow us to load the layout in parallel with the flamegraph data.
|
|
165
|
+
// However, the layout calculation is dependent on the width and color range of the graph container, which is why it is done at this level
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
async function loadCallgraphSVG(
|
|
168
|
+
graph: CallgraphType,
|
|
169
|
+
width: number,
|
|
170
|
+
colorRange: [string, string]
|
|
171
|
+
): Promise<void> {
|
|
172
|
+
await setCallgraphSVG(undefined);
|
|
173
|
+
// Translate JSON to 'dot' graph string
|
|
174
|
+
const dataAsDot = await jsonToDot({
|
|
175
|
+
graph,
|
|
176
|
+
width,
|
|
177
|
+
colorRange,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Use Graphviz-WASM to translate the 'dot' graph to a 'JSON' graph
|
|
181
|
+
const svgGraph = await graphviz.layout(dataAsDot, 'svg', 'dot');
|
|
182
|
+
await setCallgraphSVG(svgGraph);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
graphvizLoaded &&
|
|
187
|
+
callgraphData?.data !== null &&
|
|
188
|
+
callgraphData?.data !== undefined &&
|
|
189
|
+
dimensions?.width !== undefined
|
|
190
|
+
) {
|
|
191
|
+
void loadCallgraphSVG(callgraphData?.data, dimensions?.width, colorRange);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
195
|
+
}, [graphvizLoaded, callgraphData?.data]);
|
|
196
|
+
|
|
197
|
+
if (flamegraphData?.error !== null) {
|
|
143
198
|
console.error('Error: ', flamegraphData?.error);
|
|
144
199
|
return (
|
|
145
200
|
<div className="p-10 flex justify-center">
|
|
@@ -154,10 +209,6 @@ export const ProfileView = ({
|
|
|
154
209
|
}
|
|
155
210
|
};
|
|
156
211
|
|
|
157
|
-
const maxColor: string = getNewSpanColor(isDarkMode);
|
|
158
|
-
const minColor: string = scaleLinear([isDarkMode ? 'black' : 'white', maxColor])(0.3);
|
|
159
|
-
const colorRange: [string, string] = [minColor, maxColor];
|
|
160
|
-
|
|
161
212
|
const getDashboardItemByType = ({
|
|
162
213
|
type,
|
|
163
214
|
isHalfScreen,
|
|
@@ -194,12 +245,14 @@ export const ProfileView = ({
|
|
|
194
245
|
);
|
|
195
246
|
}
|
|
196
247
|
case 'callgraph': {
|
|
197
|
-
return callgraphData?.data
|
|
248
|
+
return callgraphData?.data !== undefined &&
|
|
249
|
+
callgraphSVG !== undefined &&
|
|
250
|
+
dimensions?.width !== undefined ? (
|
|
198
251
|
<Callgraph
|
|
199
|
-
|
|
252
|
+
data={callgraphData.data}
|
|
253
|
+
svgString={callgraphSVG}
|
|
200
254
|
sampleUnit={sampleUnit}
|
|
201
255
|
width={isHalfScreen ? dimensions?.width / 2 : dimensions?.width}
|
|
202
|
-
colorRange={colorRange}
|
|
203
256
|
/>
|
|
204
257
|
) : (
|
|
205
258
|
<></>
|
|
@@ -253,7 +306,7 @@ export const ProfileView = ({
|
|
|
253
306
|
<div className="flex py-3 w-full">
|
|
254
307
|
<div className="lg:w-1/2 flex space-x-4">
|
|
255
308
|
<div className="flex space-x-1">
|
|
256
|
-
{profileSource
|
|
309
|
+
{profileSource !== undefined && queryClient !== undefined ? (
|
|
257
310
|
<ProfileShareButton
|
|
258
311
|
queryRequest={profileSource.QueryRequest()}
|
|
259
312
|
queryClient={queryClient}
|
|
@@ -287,11 +340,11 @@ export const ProfileView = ({
|
|
|
287
340
|
</div>
|
|
288
341
|
</div>
|
|
289
342
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
<
|
|
343
|
+
<div className="w-full" ref={ref}>
|
|
344
|
+
{isLoaderVisible ? (
|
|
345
|
+
<>{loader}</>
|
|
346
|
+
) : (
|
|
347
|
+
<DragDropContext onDragEnd={onDragEnd}>
|
|
295
348
|
<Droppable droppableId="droppable" direction="horizontal">
|
|
296
349
|
{provided => (
|
|
297
350
|
<div
|
|
@@ -335,9 +388,9 @@ export const ProfileView = ({
|
|
|
335
388
|
</div>
|
|
336
389
|
)}
|
|
337
390
|
</Droppable>
|
|
338
|
-
</
|
|
339
|
-
|
|
340
|
-
|
|
391
|
+
</DragDropContext>
|
|
392
|
+
)}
|
|
393
|
+
</div>
|
|
341
394
|
</Card.Body>
|
|
342
395
|
</Card>
|
|
343
396
|
</div>
|
package/typings.d.ts
CHANGED