@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.
@@ -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 {CallgraphNode, FlamegraphNode, FlamegraphNodeMeta, FlamegraphRootNode} from '@parca/client';
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: FlamegraphNode;
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: FlamegraphNode): string => {
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 = (hoveringNodeProp ?? hoveringNodeState) as HoveringNode;
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 hoveringNode={hoveringNode} unit={unit} total={total} isFixed={isFixed} />
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
- dispatch(setHoveringNode(data));
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
- }, [dashboardItems, callgraphData?.loading, flamegraphData?.loading, topTableData?.loading]);
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
- if (flamegraphData?.error != null) {
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 != null && dimensions?.width !== undefined ? (
248
+ return callgraphData?.data !== undefined &&
249
+ callgraphSVG !== undefined &&
250
+ dimensions?.width !== undefined ? (
198
251
  <Callgraph
199
- graph={callgraphData.data}
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 != null && queryClient != null ? (
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
- {isLoaderVisible ? (
291
- <>{loader}</>
292
- ) : (
293
- <DragDropContext onDragEnd={onDragEnd}>
294
- <div className="w-full" ref={ref}>
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
- </div>
339
- </DragDropContext>
340
- )}
391
+ </DragDropContext>
392
+ )}
393
+ </div>
341
394
  </Card.Body>
342
395
  </Card>
343
396
  </div>
package/typings.d.ts CHANGED
@@ -12,3 +12,4 @@
12
12
  // limitations under the License.
13
13
 
14
14
  declare module '*.svg';
15
+ declare module 'react-map-interaction';