@parca/profile 0.19.44 → 0.19.46

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/GraphTooltipArrow/Content.d.ts.map +1 -1
  3. package/dist/GraphTooltipArrow/Content.js +1 -1
  4. package/dist/MetricsGraph/MetricsContextMenu/index.d.ts +20 -11
  5. package/dist/MetricsGraph/MetricsContextMenu/index.d.ts.map +1 -1
  6. package/dist/MetricsGraph/MetricsContextMenu/index.js +16 -20
  7. package/dist/MetricsGraph/MetricsTooltip/index.d.ts +2 -8
  8. package/dist/MetricsGraph/MetricsTooltip/index.d.ts.map +1 -1
  9. package/dist/MetricsGraph/MetricsTooltip/index.js +46 -55
  10. package/dist/MetricsGraph/UtilizationMetrics/Throughput.d.ts +2 -5
  11. package/dist/MetricsGraph/UtilizationMetrics/Throughput.d.ts.map +1 -1
  12. package/dist/MetricsGraph/UtilizationMetrics/Throughput.js +126 -205
  13. package/dist/MetricsGraph/UtilizationMetrics/index.d.ts +9 -17
  14. package/dist/MetricsGraph/UtilizationMetrics/index.d.ts.map +1 -1
  15. package/dist/MetricsGraph/UtilizationMetrics/index.js +149 -208
  16. package/dist/MetricsGraph/index.d.ts +19 -26
  17. package/dist/MetricsGraph/index.d.ts.map +1 -1
  18. package/dist/MetricsGraph/index.js +50 -115
  19. package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
  20. package/dist/ProfileFlameGraph/index.js +3 -1
  21. package/dist/ProfileMetricsGraph/index.d.ts +1 -1
  22. package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
  23. package/dist/ProfileMetricsGraph/index.js +232 -23
  24. package/dist/ProfileSelector/MetricsGraphSection.d.ts +1 -4
  25. package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
  26. package/dist/ProfileSelector/MetricsGraphSection.js +8 -4
  27. package/dist/ProfileSelector/index.d.ts +3 -6
  28. package/dist/ProfileSelector/index.d.ts.map +1 -1
  29. package/dist/ProfileSelector/index.js +2 -2
  30. package/dist/ProfileSource.d.ts +9 -6
  31. package/dist/ProfileSource.d.ts.map +1 -1
  32. package/dist/ProfileSource.js +23 -8
  33. package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts.map +1 -1
  34. package/dist/ProfileView/components/GroupByLabelsDropdown/index.js +5 -1
  35. package/dist/ProfileView/components/ProfileFilters/index.d.ts.map +1 -1
  36. package/dist/ProfileView/components/ProfileFilters/index.js +6 -5
  37. package/dist/styles.css +1 -1
  38. package/dist/useQuery.js +1 -1
  39. package/package.json +7 -7
  40. package/src/GraphTooltipArrow/Content.tsx +2 -4
  41. package/src/MetricsGraph/MetricsContextMenu/index.tsx +78 -66
  42. package/src/MetricsGraph/MetricsTooltip/index.tsx +53 -210
  43. package/src/MetricsGraph/UtilizationMetrics/Throughput.tsx +242 -434
  44. package/src/MetricsGraph/UtilizationMetrics/index.tsx +312 -448
  45. package/src/MetricsGraph/index.tsx +99 -185
  46. package/src/ProfileFlameGraph/index.tsx +3 -1
  47. package/src/ProfileMetricsGraph/index.tsx +430 -37
  48. package/src/ProfileSelector/MetricsGraphSection.tsx +12 -8
  49. package/src/ProfileSelector/index.tsx +5 -5
  50. package/src/ProfileSource.tsx +34 -17
  51. package/src/ProfileView/components/GroupByLabelsDropdown/index.tsx +15 -3
  52. package/src/ProfileView/components/ProfileFilters/index.tsx +23 -3
  53. package/src/useQuery.tsx +1 -1
@@ -14,92 +14,104 @@
14
14
  import {Icon} from '@iconify/react';
15
15
  import {Item, Menu, Submenu} from 'react-contexify';
16
16
 
17
- import {Label} from '@parca/client';
18
17
  import {useParcaContext} from '@parca/components';
19
18
 
20
- import {HighlightedSeries} from '../';
19
+ import {Series, SeriesPoint} from '../';
20
+
21
+ export interface ContextMenuItem {
22
+ id: string;
23
+ label: React.ReactNode;
24
+ icon?: string;
25
+ onClick: (closestPoint: SeriesPoint | null, series: Series[]) => void;
26
+ disabled?: (closestPoint: SeriesPoint | null, series: Series[]) => boolean;
27
+ }
28
+
29
+ export interface ContextMenuSubmenu {
30
+ id: string;
31
+ label: React.ReactNode;
32
+ icon?: string;
33
+ items?: ContextMenuItem[];
34
+ createDynamicItems?: (closestPoint: SeriesPoint | null, series: Series[]) => ContextMenuItem[];
35
+ }
36
+
37
+ export type ContextMenuItemOrSubmenu = ContextMenuItem | ContextMenuSubmenu;
21
38
 
22
39
  interface MetricsContextMenuProps {
23
40
  menuId: string;
24
- onAddLabelMatcher: (
25
- labels: {key: string; value: string} | Array<{key: string; value: string}>
26
- ) => void;
27
- highlighted: HighlightedSeries | null;
41
+ closestPoint: SeriesPoint | null;
42
+ series: Series[];
28
43
  trackVisibility: (isVisible: boolean) => void;
29
- utilizationMetrics?: boolean;
44
+ menuItems: ContextMenuItemOrSubmenu[];
30
45
  }
31
46
 
32
- const transformUtilizationLabels = (label: string, utilizationMetrics: boolean): string => {
33
- if (utilizationMetrics) {
34
- return label.replace('attributes.', '').replace('attributes_resource.', '');
35
- }
36
- return label;
37
- };
38
-
39
47
  const MetricsContextMenu = ({
40
48
  menuId,
41
- onAddLabelMatcher,
42
- highlighted,
49
+ closestPoint,
50
+ series,
43
51
  trackVisibility,
44
- utilizationMetrics = false,
52
+ menuItems,
45
53
  }: MetricsContextMenuProps): JSX.Element => {
46
54
  const {isDarkMode} = useParcaContext();
47
- const labels = highlighted?.labels.filter((label: Label) => label.name !== '__name__');
48
55
 
49
- const handleFocusOnSingleSeries = (): void => {
50
- const labelsToAdd = labels?.map((label: Label) => ({
51
- key: label.name,
52
- value: label.value,
53
- }));
56
+ const renderMenuItem = (item: ContextMenuItemOrSubmenu): React.ReactNode => {
57
+ if ('items' in item || 'createDynamicItems' in item) {
58
+ // This is a submenu
59
+ const submenu = item;
60
+ const items =
61
+ submenu.createDynamicItems != null
62
+ ? submenu.createDynamicItems(closestPoint, series)
63
+ : submenu.items ?? [];
54
64
 
55
- labelsToAdd !== undefined && onAddLabelMatcher(labelsToAdd);
56
- };
57
-
58
- return (
59
- <Menu id={menuId} onVisibilityChange={trackVisibility} theme={isDarkMode ? 'dark' : ''}>
60
- <Item id="focus-on-single-series" onClick={handleFocusOnSingleSeries}>
61
- <div className="flex w-full items-center gap-2">
62
- <Icon icon="ph:star" />
63
- <div>Focus only on this series</div>
64
- </div>
65
- </Item>
66
- <Submenu
67
- label={
68
- <div className="flex w-full items-center gap-2">
69
- <Icon icon="material-symbols:add" />
70
- <div>Add to query</div>
71
- </div>
72
- }
73
- >
74
- <div className="max-h-[300px] overflow-auto">
75
- {labels == null || labels.length === 0 ? (
76
- <Item disabled>
77
- <div className="flex w-full items-center gap-2">
78
- <Icon icon="ph:warning" />
79
- <div>No labels available</div>
80
- </div>
81
- </Item>
82
- ) : (
83
- labels.map((label: Label) => (
65
+ return (
66
+ <Submenu
67
+ key={submenu.id}
68
+ label={
69
+ <div className="flex w-full items-center gap-2">
70
+ {submenu.icon != null && submenu.icon !== '' && <Icon icon={submenu.icon} />}
71
+ <div>{submenu.label}</div>
72
+ </div>
73
+ }
74
+ >
75
+ <div className="max-h-[300px] overflow-auto">
76
+ {items.map(subItem => (
84
77
  <Item
85
- key={label.name}
86
- id={label.name}
87
- onClick={() => {
88
- onAddLabelMatcher({
89
- key: label.name,
90
- value: label.value,
91
- });
92
- }}
78
+ key={subItem.id}
79
+ id={subItem.id}
80
+ onClick={() => subItem.onClick(closestPoint, series)}
81
+ disabled={subItem.disabled?.(closestPoint, series) ?? false}
93
82
  className="max-w-[400px] overflow-hidden"
94
83
  >
95
- <div className="mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-300">
96
- {`${transformUtilizationLabels(label.name, utilizationMetrics)}="${label.value}"`}
84
+ <div className="flex w-full items-center gap-2">
85
+ {subItem.icon != null && subItem.icon !== '' && <Icon icon={subItem.icon} />}
86
+ <div>{subItem.label}</div>
97
87
  </div>
98
88
  </Item>
99
- ))
100
- )}
101
- </div>
102
- </Submenu>
89
+ ))}
90
+ </div>
91
+ </Submenu>
92
+ );
93
+ } else {
94
+ // This is a regular menu item
95
+ const menuItem = item as ContextMenuItem;
96
+ return (
97
+ <Item
98
+ key={menuItem.id}
99
+ id={menuItem.id}
100
+ onClick={() => menuItem.onClick(closestPoint, series)}
101
+ disabled={menuItem.disabled?.(closestPoint, series) ?? false}
102
+ >
103
+ <div className="flex w-full items-center gap-2">
104
+ {menuItem.icon != null && menuItem.icon !== '' && <Icon icon={menuItem.icon} />}
105
+ <div>{menuItem.label}</div>
106
+ </div>
107
+ </Item>
108
+ );
109
+ }
110
+ };
111
+
112
+ return (
113
+ <Menu id={menuId} onVisibilityChange={trackVisibility} theme={isDarkMode ? 'dark' : ''}>
114
+ {menuItems.map(renderMenuItem)}
103
115
  </Menu>
104
116
  );
105
117
  };
@@ -13,262 +13,105 @@
13
13
 
14
14
  import {useEffect, useMemo, useState} from 'react';
15
15
 
16
- import {Icon} from '@iconify/react';
17
- import type {VirtualElement} from '@popperjs/core';
18
16
  import {usePopper} from 'react-popper';
19
17
 
20
- import {Label} from '@parca/client';
21
- import {TextWithTooltip, useParcaContext} from '@parca/components';
22
- import {formatDate, timePattern, valueFormatter} from '@parca/utilities';
23
-
24
- import {HighlightedSeries} from '../';
18
+ interface VirtualElement {
19
+ getBoundingClientRect: () => DOMRect;
20
+ }
25
21
 
26
22
  interface Props {
27
23
  x: number;
28
24
  y: number;
29
- highlighted: HighlightedSeries;
30
25
  contextElement: Element | null;
31
- sampleType: string;
32
- sampleUnit: string;
33
- delta: boolean;
34
- utilizationMetrics?: boolean;
35
- valuePrefix?: string;
26
+ content: React.ReactNode;
36
27
  }
37
28
 
38
29
  const virtualElement: VirtualElement = {
39
30
  getBoundingClientRect: () => {
40
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
41
- return {
31
+ const emptyRect: DOMRect = {
42
32
  width: 0,
43
33
  height: 0,
44
34
  top: 0,
45
- left: 0,
46
35
  right: 0,
47
36
  bottom: 0,
48
- } as DOMRect;
37
+ left: 0,
38
+ x: 0,
39
+ y: 0,
40
+ toJSON: () => ({}),
41
+ };
42
+ return emptyRect;
49
43
  },
50
44
  };
51
45
 
52
- function generateGetBoundingClientRect(contextElement: Element, x = 0, y = 0): () => DOMRect {
53
- const domRect = contextElement.getBoundingClientRect();
54
- return () =>
55
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
56
- ({
57
- width: 0,
58
- height: 0,
59
- top: domRect.y + y,
60
- left: domRect.x + x,
61
- right: domRect.x + x,
62
- bottom: domRect.y + y,
63
- } as DOMRect);
64
- }
65
-
66
- const MetricsTooltip = ({
67
- x,
68
- y,
69
- highlighted,
70
- contextElement,
71
- sampleType,
72
- sampleUnit,
73
- delta,
74
- utilizationMetrics = false,
75
- valuePrefix,
76
- }: Props): JSX.Element => {
77
- const {timezone} = useParcaContext();
46
+ const createDomRect = (x: number, y: number): DOMRect => {
47
+ const domRect: DOMRect = {
48
+ width: 0,
49
+ height: 0,
50
+ top: y,
51
+ right: x,
52
+ bottom: y,
53
+ left: x,
54
+ x,
55
+ y,
56
+ toJSON: () => ({}),
57
+ };
58
+ return domRect;
59
+ };
78
60
 
61
+ const MetricsTooltip = ({x, y, contextElement, content}: Props): JSX.Element => {
79
62
  const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
80
63
 
81
- const {styles, attributes, ...popperProps} = usePopper(virtualElement, popperElement, {
64
+ const {styles, attributes, update} = usePopper(virtualElement, popperElement, {
82
65
  placement: 'auto-start',
83
66
  strategy: 'absolute',
84
67
  modifiers: [
85
68
  {
86
69
  name: 'preventOverflow',
87
70
  options: {
88
- tether: false,
89
- altAxis: true,
71
+ boundary: contextElement ?? undefined,
90
72
  },
91
73
  },
92
74
  {
93
75
  name: 'offset',
94
76
  options: {
95
- offset: [30, 30],
77
+ offset: [15, 15],
96
78
  },
97
79
  },
98
80
  ],
99
81
  });
100
82
 
101
- const update = popperProps.update;
102
-
103
- const attributesMap = useMemo(() => {
104
- return highlighted.labels
105
- .filter(
106
- label =>
107
- label.name.startsWith('attributes.') && !label.name.startsWith('attributes_resource.')
108
- )
109
- .reduce<Record<string, string>>((acc, label) => {
110
- const key = label.name.replace('attributes.', '');
111
- acc[key] = label.value;
112
- return acc;
113
- }, {});
114
- }, [highlighted.labels]);
115
-
116
- const attributesResourceMap = useMemo(() => {
117
- return highlighted.labels
118
- .filter(label => label.name.startsWith('attributes_resource.'))
119
- .reduce<Record<string, string>>((acc, label) => {
120
- const key = label.name.replace('attributes_resource.', '');
121
- acc[key] = label.value;
122
- return acc;
123
- }, {});
124
- }, [highlighted.labels]);
83
+ useMemo(() => {
84
+ virtualElement.getBoundingClientRect = (): DOMRect => {
85
+ const domRect: DOMRect = (contextElement as Element)?.getBoundingClientRect() ?? {
86
+ width: 0,
87
+ height: 0,
88
+ top: 0,
89
+ right: 0,
90
+ bottom: 0,
91
+ left: 0,
92
+ x: 0,
93
+ y: 0,
94
+ toJSON: () => ({}),
95
+ };
96
+ return createDomRect(domRect.x + x, domRect.y + y);
97
+ };
98
+ }, [x, y, contextElement]);
125
99
 
126
100
  useEffect(() => {
127
- if (contextElement != null) {
128
- virtualElement.getBoundingClientRect = generateGetBoundingClientRect(contextElement, x, y);
129
- void update?.();
130
- }
131
- }, [x, y, contextElement, update]);
101
+ void update?.();
102
+ }, [x, y, update]);
132
103
 
133
- const nameLabel: Label | undefined = highlighted?.labels.find(e => e.name === '__name__');
134
- const highlightedNameLabel: Label = nameLabel !== undefined ? nameLabel : {name: '', value: ''};
104
+ // Don't render anything if content is null or undefined
105
+ if (content == null) {
106
+ return <></>;
107
+ }
135
108
 
136
109
  return (
137
110
  <div ref={setPopperElement} style={styles.popper} {...attributes.popper} className="z-50">
138
111
  <div className="flex max-w-lg">
139
112
  <div className="m-auto">
140
- <div
141
- className="rounded-lg border-gray-300 bg-gray-50 p-3 opacity-90 shadow-lg dark:border-gray-500 dark:bg-gray-900"
142
- style={{borderWidth: 1}}
143
- >
144
- <div className="flex flex-row">
145
- <div className="ml-2 mr-6">
146
- <span className="font-semibold">{highlightedNameLabel.value}</span>
147
- <span className="my-2 block text-gray-700 dark:text-gray-300">
148
- <table className="table-auto">
149
- <tbody>
150
- {delta ? (
151
- <>
152
- <tr>
153
- <td className="w-1/4 pr-3">Per&nbsp;Second</td>
154
- <td className="w-3/4">
155
- {valueFormatter(
156
- highlighted.valuePerSecond,
157
- sampleUnit === 'nanoseconds' && sampleType === 'cpu'
158
- ? 'CPU Cores'
159
- : sampleUnit,
160
- 5
161
- )}
162
- </td>
163
- </tr>
164
- <tr>
165
- <td className="w-1/4">Total</td>
166
- <td className="w-3/4">
167
- {valueFormatter(highlighted.value, sampleUnit, 2)}
168
- </td>
169
- </tr>
170
- </>
171
- ) : (
172
- <tr>
173
- <td className="w-1/4">
174
- {valuePrefix ?? ''}
175
- Value
176
- </td>
177
- <td className="w-3/4">
178
- {valueFormatter(highlighted.valuePerSecond, sampleUnit, 5)}
179
- </td>
180
- </tr>
181
- )}
182
- {highlighted.duration > 0 && (
183
- <tr>
184
- <td className="w-1/4">Duration</td>
185
- <td className="w-3/4">
186
- {valueFormatter(highlighted.duration, 'nanoseconds', 2)}
187
- </td>
188
- </tr>
189
- )}
190
- <tr>
191
- <td className="w-1/4">At</td>
192
- <td className="w-3/4">
193
- {formatDate(
194
- highlighted.timestamp,
195
- timePattern(timezone as string),
196
- timezone
197
- )}
198
- </td>
199
- </tr>
200
- </tbody>
201
- </table>
202
- </span>
203
- <span className="my-2 block text-gray-500">
204
- {utilizationMetrics ? (
205
- <>
206
- {Object.keys(attributesResourceMap).length > 0 && (
207
- <span className="text-sm font-bold text-gray-700 dark:text-white">
208
- Resource Attributes
209
- </span>
210
- )}
211
- <span className="my-2 block text-gray-500">
212
- {Object.keys(attributesResourceMap).map(name => (
213
- <div
214
- key={name}
215
- className="mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400"
216
- >
217
- <TextWithTooltip
218
- text={`${name.replace('attributes.', '')}="${
219
- attributesResourceMap[name]
220
- }"`}
221
- maxTextLength={48}
222
- id={`tooltip-${name}-${attributesResourceMap[name]}`}
223
- />
224
- </div>
225
- ))}
226
- </span>
227
- {Object.keys(attributesMap).length > 0 && (
228
- <span className="text-sm font-bold text-gray-700 dark:text-white">
229
- Attributes
230
- </span>
231
- )}
232
- <span className="my-2 block text-gray-500">
233
- {Object.keys(attributesMap).map(name => (
234
- <div
235
- key={name}
236
- className="mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400"
237
- >
238
- <TextWithTooltip
239
- text={`${name.replace('attributes.', '')}="${attributesMap[name]}"`}
240
- maxTextLength={48}
241
- id={`tooltip-${name}-${attributesMap[name]}`}
242
- />
243
- </div>
244
- ))}
245
- </span>
246
- </>
247
- ) : (
248
- <>
249
- {highlighted.labels
250
- .filter((label: Label) => label.name !== '__name__')
251
- .map((label: Label) => (
252
- <div
253
- key={label.name}
254
- className="mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400"
255
- >
256
- <TextWithTooltip
257
- text={`${label.name}="${label.value}"`}
258
- maxTextLength={37}
259
- id={`tooltip-${label.name}`}
260
- />
261
- </div>
262
- ))}
263
- </>
264
- )}
265
- </span>
266
- <div className="flex w-full items-center gap-1 text-xs text-gray-500">
267
- <Icon icon="iconoir:mouse-button-right" />
268
- <div>Right click to add labels to query.</div>
269
- </div>
270
- </div>
271
- </div>
113
+ <div className="border border-gray-300 bg-gray-50 dark:border-gray-500 dark:bg-gray-900 rounded-lg shadow-lg px-3 py-2">
114
+ {content}
272
115
  </div>
273
116
  </div>
274
117
  </div>