@parca/profile 0.19.44 → 0.19.45

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 (47) hide show
  1. package/CHANGELOG.md +4 -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/styles.css +1 -1
  34. package/dist/useQuery.js +1 -1
  35. package/package.json +6 -6
  36. package/src/GraphTooltipArrow/Content.tsx +2 -4
  37. package/src/MetricsGraph/MetricsContextMenu/index.tsx +78 -66
  38. package/src/MetricsGraph/MetricsTooltip/index.tsx +53 -210
  39. package/src/MetricsGraph/UtilizationMetrics/Throughput.tsx +242 -434
  40. package/src/MetricsGraph/UtilizationMetrics/index.tsx +312 -448
  41. package/src/MetricsGraph/index.tsx +99 -185
  42. package/src/ProfileFlameGraph/index.tsx +3 -1
  43. package/src/ProfileMetricsGraph/index.tsx +430 -37
  44. package/src/ProfileSelector/MetricsGraphSection.tsx +12 -8
  45. package/src/ProfileSelector/index.tsx +5 -5
  46. package/src/ProfileSource.tsx +34 -17
  47. package/src/useQuery.tsx +1 -1
@@ -18,73 +18,66 @@ import {pointer} from 'd3-selection';
18
18
  import throttle from 'lodash.throttle';
19
19
  import {useContextMenu} from 'react-contexify';
20
20
 
21
- import {Label, MetricsSample, MetricsSeries as MetricsSeriesPb} from '@parca/client';
22
21
  import {DateTimeRange, useParcaContext} from '@parca/components';
23
- import {
24
- formatDate,
25
- formatForTimespan,
26
- getPrecision,
27
- sanitizeHighlightedValues,
28
- valueFormatter,
29
- } from '@parca/utilities';
30
-
31
- import {MergedProfileSelection} from '..';
22
+ import {formatDate, formatForTimespan, getPrecision, valueFormatter} from '@parca/utilities';
23
+
32
24
  import MetricsCircle from '../MetricsCircle';
33
25
  import MetricsSeries from '../MetricsSeries';
34
- import MetricsContextMenu from './MetricsContextMenu';
26
+ import MetricsContextMenu, {
27
+ ContextMenuItem,
28
+ ContextMenuItemOrSubmenu,
29
+ ContextMenuSubmenu,
30
+ } from './MetricsContextMenu';
35
31
  import MetricsInfoPanel from './MetricsInfoPanel';
36
32
  import MetricsTooltip from './MetricsTooltip';
37
33
 
38
34
  interface Props {
39
- data: MetricsSeriesPb[];
35
+ data: Series[];
40
36
  from: number;
41
37
  to: number;
42
- profile: MergedProfileSelection | null;
43
- onSampleClick: (timestamp: number, value: number, labels: Label[], duration: number) => void;
44
- addLabelMatcher: (
45
- labels: {key: string; value: string} | Array<{key: string; value: string}>
46
- ) => void;
38
+ onSampleClick: (closestPoint: SeriesPoint) => void;
47
39
  setTimeRange: (range: DateTimeRange) => void;
48
- sampleType: string;
49
- sampleUnit: string;
40
+ yAxisLabel: string;
41
+ yAxisUnit: string;
50
42
  width?: number;
51
43
  height?: number;
52
44
  margin?: number;
53
- sumBy?: string[];
45
+ selectedPoint?: SeriesPoint | null;
46
+ contextMenuItems?: ContextMenuItemOrSubmenu[];
47
+ renderTooltipContent?: (seriesIndex: number, pointIndex: number) => React.ReactNode;
48
+ }
49
+
50
+ export interface SeriesPoint {
51
+ seriesIndex: number;
52
+ pointIndex: number;
54
53
  }
55
54
 
56
55
  export interface HighlightedSeries {
57
56
  seriesIndex: number;
58
- labels: Label[];
59
- timestamp: number;
60
- value: number;
61
- valuePerSecond: number;
62
- duration: number;
57
+ pointIndex: number;
63
58
  x: number;
64
59
  y: number;
65
60
  }
66
61
 
67
62
  export interface Series {
68
- metric: Label[];
69
- values: number[][];
70
- labelset: string;
71
- isSelected?: boolean;
63
+ id: string; // opaque string used to determine line color
64
+ values: Array<[number, number]>; // [timestamp_ms, value]
72
65
  }
73
66
 
74
67
  const MetricsGraph = ({
75
68
  data,
76
69
  from,
77
70
  to,
78
- profile,
79
71
  onSampleClick,
80
- addLabelMatcher,
81
72
  setTimeRange,
82
- sampleType,
83
- sampleUnit,
73
+ yAxisLabel,
74
+ yAxisUnit,
84
75
  width = 0,
85
76
  height = 0,
86
77
  margin = 0,
87
- sumBy,
78
+ selectedPoint,
79
+ contextMenuItems,
80
+ renderTooltipContent,
88
81
  }: Props): JSX.Element => {
89
82
  const [isInfoPanelOpen, setIsInfoPanelOpen] = useState<boolean>(false);
90
83
  return (
@@ -99,22 +92,23 @@ const MetricsGraph = ({
99
92
  data={data}
100
93
  from={from}
101
94
  to={to}
102
- profile={profile}
103
95
  onSampleClick={onSampleClick}
104
- addLabelMatcher={addLabelMatcher}
105
96
  setTimeRange={setTimeRange}
106
- sampleType={sampleType}
107
- sampleUnit={sampleUnit}
97
+ yAxisLabel={yAxisLabel}
98
+ yAxisUnit={yAxisUnit}
108
99
  width={width}
109
100
  height={height}
110
101
  margin={margin}
111
- sumBy={sumBy}
102
+ selectedPoint={selectedPoint}
103
+ contextMenuItems={contextMenuItems}
104
+ renderTooltipContent={renderTooltipContent}
112
105
  />
113
106
  </div>
114
107
  );
115
108
  };
116
109
 
117
110
  export default MetricsGraph;
111
+ export type {ContextMenuItemOrSubmenu, ContextMenuItem, ContextMenuSubmenu};
118
112
 
119
113
  export const parseValue = (value: string): number | null => {
120
114
  const val = parseFloat(value);
@@ -130,16 +124,16 @@ export const RawMetricsGraph = ({
130
124
  data,
131
125
  from,
132
126
  to,
133
- profile,
134
127
  onSampleClick,
135
- addLabelMatcher,
136
128
  setTimeRange,
137
- sampleType,
138
- sampleUnit,
129
+ yAxisLabel,
130
+ yAxisUnit,
139
131
  width,
140
132
  height = 50,
141
133
  margin = 0,
142
- sumBy,
134
+ selectedPoint,
135
+ contextMenuItems,
136
+ renderTooltipContent,
143
137
  }: Props): JSX.Element => {
144
138
  const {timezone} = useParcaContext();
145
139
  const graph = useRef(null);
@@ -151,9 +145,6 @@ export const RawMetricsGraph = ({
151
145
  const metricPointRef = useRef(null);
152
146
  const idForContextMenu = useId();
153
147
 
154
- // the time of the selected point is the start of the merge window
155
- const time: number = parseFloat(profile?.HistoryParams().merge_from);
156
-
157
148
  if (width === undefined || width == null) {
158
149
  width = 0;
159
150
  }
@@ -164,30 +155,11 @@ export const RawMetricsGraph = ({
164
155
  return `translate(6, 0) scale(${(graphWidth - 6) / graphWidth}, 1)`;
165
156
  }, [graphWidth]);
166
157
 
167
- const series: Series[] = data.reduce<Series[]>(function (agg: Series[], s: MetricsSeriesPb) {
168
- if (s.labelset !== undefined) {
169
- const metric = s.labelset.labels.sort((a, b) => a.name.localeCompare(b.name));
170
- agg.push({
171
- metric,
172
- values: s.samples.reduce<number[][]>(function (agg: number[][], d: MetricsSample) {
173
- if (d.timestamp !== undefined && d.valuePerSecond !== undefined) {
174
- const t = (Number(d.timestamp.seconds) * 1e9 + d.timestamp.nanos) / 1e6; // https://github.com/microsoft/TypeScript/issues/5710#issuecomment-157886246
175
- agg.push([t, d.valuePerSecond, Number(d.value), Number(d.duration)]);
176
- }
177
- return agg;
178
- }, []),
179
- labelset: metric.map(m => `${m.name}=${m.value}`).join(','),
180
- });
181
- }
182
- return agg;
183
- }, []);
184
-
185
- // Sort series by id to make sure the colors are consistent
186
- series.sort((a, b) => a.labelset.localeCompare(b.labelset));
158
+ const series = data;
187
159
 
188
160
  const extentsY = series.map(function (s) {
189
161
  return d3.extent(s.values, function (d) {
190
- return d[1];
162
+ return d[1]; // d[1] is the value
191
163
  });
192
164
  });
193
165
 
@@ -208,21 +180,29 @@ export const RawMetricsGraph = ({
208
180
  .range([height - margin, 0])
209
181
  .nice();
210
182
 
211
- const color = d3.scaleOrdinal(d3.schemeCategory10);
183
+ // Create deterministic color mapping based on series IDs
184
+ const color = useMemo(() => {
185
+ const scale = d3.scaleOrdinal(d3.schemeCategory10);
186
+ // Pre-populate the scale with sorted series IDs to ensure consistent colors
187
+ const sortedIds = [...new Set(series.map(s => s.id))].sort();
188
+ sortedIds.forEach(id => scale(id));
189
+ return scale;
190
+ }, [series]);
212
191
 
213
- const l = d3.line(
192
+ const l = d3.line<[number, number]>(
214
193
  d => xScale(d[0]),
215
194
  d => yScale(d[1])
216
195
  );
217
196
 
218
- const highlighted = useMemo(() => {
197
+ const closestPoint = useMemo(() => {
219
198
  // Return the closest point as the highlighted point
220
199
 
221
200
  const closestPointPerSeries = series.map(function (s) {
222
201
  const distances = s.values.map(d => {
223
- const x = xScale(d[0]) + margin / 2;
224
- const y = yScale(d[1]) - margin / 3;
202
+ const x = xScale(d[0]) + margin / 2; // d[0] is timestamp_ms
203
+ const y = yScale(d[1]) - margin / 3; // d[1] is value
225
204
 
205
+ // Cartesian distance from the mouse position to the point
226
206
  return Math.sqrt(Math.pow(pos[0] - x, 2) + Math.pow(pos[1] - y, 2));
227
207
  });
228
208
 
@@ -237,18 +217,39 @@ export const RawMetricsGraph = ({
237
217
 
238
218
  const closestSeriesIndex = d3.minIndex(closestPointPerSeries, s => s.distance);
239
219
  const pointIndex = closestPointPerSeries[closestSeriesIndex].pointIndex;
240
- const point = series[closestSeriesIndex].values[pointIndex];
241
220
  return {
242
221
  seriesIndex: closestSeriesIndex,
243
- labels: series[closestSeriesIndex].metric,
244
- timestamp: point[0],
245
- valuePerSecond: point[1],
246
- value: point[2],
247
- duration: point[3],
222
+ pointIndex,
223
+ };
224
+ }, [pos, series, xScale, yScale, margin]);
225
+
226
+ const highlighted = useMemo(() => {
227
+ if (series.length === 0 || closestPoint == null) {
228
+ return null;
229
+ }
230
+
231
+ const point = series[closestPoint.seriesIndex].values[closestPoint.pointIndex];
232
+ return {
233
+ seriesIndex: closestPoint.seriesIndex,
234
+ pointIndex: closestPoint.pointIndex,
248
235
  x: xScale(point[0]),
249
236
  y: yScale(point[1]),
250
237
  };
251
- }, [pos, series, xScale, yScale, margin]);
238
+ }, [closestPoint, series, xScale, yScale]);
239
+
240
+ const selected = useMemo(() => {
241
+ if (series.length === 0 || selectedPoint == null) {
242
+ return null;
243
+ }
244
+
245
+ const point = series[selectedPoint.seriesIndex].values[selectedPoint.pointIndex];
246
+ return {
247
+ seriesIndex: selectedPoint.seriesIndex,
248
+ pointIndex: selectedPoint.pointIndex,
249
+ x: xScale(point[0]),
250
+ y: yScale(point[1]),
251
+ };
252
+ }, [selectedPoint, series, xScale, yScale]);
252
253
 
253
254
  const onMouseDown = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
254
255
  // only left mouse button
@@ -270,14 +271,9 @@ export const RawMetricsGraph = ({
270
271
  e.preventDefault();
271
272
  };
272
273
 
273
- const openClosestProfile = (): void => {
274
- if (highlighted != null) {
275
- onSampleClick(
276
- Math.round(highlighted.timestamp),
277
- highlighted.value,
278
- sanitizeHighlightedValues(highlighted.labels), // When a user clicks on any sample in the graph, replace single `\` in the `labelValues` string with doubles `\\` if available.
279
- highlighted.duration
280
- );
274
+ const handleClosestPointClick = (): void => {
275
+ if (closestPoint != null) {
276
+ onSampleClick(closestPoint);
281
277
  }
282
278
  };
283
279
 
@@ -292,7 +288,7 @@ export const RawMetricsGraph = ({
292
288
  // This is a normal click. We tolerate tiny movements to still be a
293
289
  // click as they can occur when clicking based on user feedback.
294
290
  if (Math.abs(relPos - pos[0]) <= 1) {
295
- openClosestProfile();
291
+ handleClosestPointClick();
296
292
  setRelPos(-1);
297
293
  return;
298
294
  }
@@ -337,69 +333,6 @@ export const RawMetricsGraph = ({
337
333
  throttledSetPos([xCoordinateWithoutMargin, yCoordinateWithoutMargin]);
338
334
  };
339
335
 
340
- const findSelectedProfile = (): HighlightedSeries | null => {
341
- if (profile == null) {
342
- return null;
343
- }
344
-
345
- let s: Series | null = null;
346
- let seriesIndex = -1;
347
-
348
- // if there are both query matchers and also a sumby value, we need to check if the sumby value is part of the query matchers.
349
- // if it is, then we should prioritize using the sumby label name and value to find the selected profile.
350
- const useSumBy =
351
- sumBy !== undefined &&
352
- sumBy.length > 0 &&
353
- profile.query.matchers.length > 0 &&
354
- profile.query.matchers.some(e => sumBy.includes(e.key));
355
-
356
- // get only the sumby keys and values from the profile query matchers
357
- const sumByMatchers =
358
- sumBy !== undefined ? profile.query.matchers.filter(e => sumBy.includes(e.key)) : [];
359
-
360
- const keysToMatch = useSumBy ? sumByMatchers : profile.query.matchers;
361
-
362
- outer: for (let i = 0; i < series.length; i++) {
363
- const keys = keysToMatch.map(e => e.key);
364
- for (let j = 0; j < keys.length; j++) {
365
- const matcherKey = keys[j];
366
- const label = series[i].metric.find(e => e.name === matcherKey);
367
- if (label === undefined) {
368
- continue outer; // label doesn't exist to begin with
369
- }
370
- if (keysToMatch[j].value !== label.value) {
371
- continue outer; // label values don't match
372
- }
373
- }
374
- seriesIndex = i;
375
- s = series[i];
376
- }
377
-
378
- if (s == null) {
379
- return null;
380
- }
381
- // Find the sample that matches the timestamp
382
- const sample = s.values.find(v => {
383
- return Math.round(v[0]) === time;
384
- });
385
- if (sample === undefined) {
386
- return null;
387
- }
388
-
389
- return {
390
- labels: [],
391
- seriesIndex,
392
- timestamp: sample[0],
393
- valuePerSecond: sample[1],
394
- value: sample[2],
395
- duration: sample[3],
396
- x: xScale(sample[0]),
397
- y: yScale(sample[1]),
398
- };
399
- };
400
-
401
- const selected = findSelectedProfile();
402
-
403
336
  const MENU_ID = `metrics-context-menu-${idForContextMenu}`;
404
337
 
405
338
  const {show} = useContextMenu({
@@ -419,33 +352,17 @@ export const RawMetricsGraph = ({
419
352
  setIsContextMenuOpen(isVisible);
420
353
  };
421
354
 
422
- const isDeltaType = profile !== null ? profile?.query.profType.delta : false;
423
-
424
- let yAxisLabel = sampleUnit;
425
- let yAxisUnit = sampleUnit;
426
- if (isDeltaType) {
427
- if (sampleUnit === 'nanoseconds') {
428
- if (sampleType === 'cpu') {
429
- yAxisLabel = 'CPU Cores';
430
- yAxisUnit = '';
431
- }
432
- if (sampleType === 'cuda') {
433
- yAxisLabel = 'GPU Time';
434
- }
435
- }
436
- if (sampleUnit === 'bytes') {
437
- yAxisLabel = 'Bytes per Second';
438
- }
439
- }
440
-
441
355
  return (
442
356
  <>
443
- <MetricsContextMenu
444
- onAddLabelMatcher={addLabelMatcher}
445
- menuId={MENU_ID}
446
- highlighted={highlighted}
447
- trackVisibility={trackVisibility}
448
- />
357
+ {contextMenuItems != null && (
358
+ <MetricsContextMenu
359
+ menuId={MENU_ID}
360
+ closestPoint={closestPoint}
361
+ series={series}
362
+ trackVisibility={trackVisibility}
363
+ menuItems={contextMenuItems}
364
+ />
365
+ )}
449
366
  {highlighted != null && hovering && !dragging && pos[0] !== 0 && pos[1] !== 0 && (
450
367
  <div
451
368
  onMouseMove={onMouseMove}
@@ -456,11 +373,8 @@ export const RawMetricsGraph = ({
456
373
  <MetricsTooltip
457
374
  x={pos[0] + margin}
458
375
  y={pos[1] + margin}
459
- highlighted={highlighted}
460
376
  contextElement={graph.current}
461
- sampleType={sampleType}
462
- sampleUnit={sampleUnit}
463
- delta={isDeltaType}
377
+ content={renderTooltipContent?.(highlighted.seriesIndex, highlighted.pointIndex)}
464
378
  />
465
379
  )}
466
380
  </div>
@@ -605,11 +519,11 @@ export const RawMetricsGraph = ({
605
519
  width={graphWidth - 100}
606
520
  >
607
521
  {series.map((s, i) => (
608
- <g key={i} className="line">
522
+ <g key={s.id} className="line">
609
523
  <MetricsSeries
610
524
  data={s}
611
525
  line={l}
612
- color={color(i.toString())}
526
+ color={color(s.id)}
613
527
  strokeWidth={
614
528
  hovering && highlighted != null && i === highlighted.seriesIndex
615
529
  ? lineStrokeHover
@@ -625,7 +539,7 @@ export const RawMetricsGraph = ({
625
539
  <g
626
540
  className="circle-group"
627
541
  ref={metricPointRef}
628
- style={{fill: color(highlighted.seriesIndex.toString())}}
542
+ style={{fill: color(series[highlighted.seriesIndex]?.id ?? '0')}}
629
543
  transform={graphTransform}
630
544
  >
631
545
  <MetricsCircle cx={highlighted.x} cy={highlighted.y} />
@@ -636,7 +550,7 @@ export const RawMetricsGraph = ({
636
550
  className="circle-group"
637
551
  style={
638
552
  selected?.seriesIndex != null
639
- ? {fill: color(selected.seriesIndex.toString())}
553
+ ? {fill: color(series[selected.seriesIndex]?.id ?? '0')}
640
554
  : {}
641
555
  }
642
556
  transform={graphTransform}
@@ -75,7 +75,9 @@ export const validateFlameChartQuery = (
75
75
  profileSource: MergedProfileSource
76
76
  ): {isValid: boolean; isNonDelta: boolean; isDurationTooLong: boolean} => {
77
77
  const isNonDelta = !profileSource.ProfileType().delta;
78
- const isDurationTooLong = profileSource.mergeTo - profileSource.mergeFrom > 60000;
78
+ const duration = profileSource.mergeTo - profileSource.mergeFrom;
79
+ console.log('duration of flame chart query: ', duration, 'ns');
80
+ const isDurationTooLong = duration > 60_000_000_000n; // 60 seconds in nanoseconds
79
81
  return {isValid: !isNonDelta && !isDurationTooLong, isNonDelta, isDurationTooLong};
80
82
  };
81
83