@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
@@ -11,20 +11,146 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {useEffect} from 'react';
14
+ import {useEffect, useMemo} from 'react';
15
15
 
16
+ import {Icon} from '@iconify/react';
16
17
  import {AnimatePresence, motion} from 'framer-motion';
17
18
 
18
- import {Label, QueryServiceClient} from '@parca/client';
19
- import {DateTimeRange, MetricsGraphSkeleton, useParcaContext} from '@parca/components';
19
+ import {
20
+ Label,
21
+ MetricsSample,
22
+ MetricsSeries as MetricsSeriesPb,
23
+ QueryServiceClient,
24
+ } from '@parca/client';
25
+ import {
26
+ DateTimeRange,
27
+ MetricsGraphSkeleton,
28
+ TextWithTooltip,
29
+ useParcaContext,
30
+ } from '@parca/components';
20
31
  import {Query} from '@parca/parser';
21
- import {capitalizeOnlyFirstLetter} from '@parca/utilities';
32
+ import {capitalizeOnlyFirstLetter, formatDate, timePattern, valueFormatter} from '@parca/utilities';
22
33
 
23
34
  import {MergedProfileSelection, ProfileSelection} from '..';
24
- import MetricsGraph from '../MetricsGraph';
35
+ import MetricsGraph, {ContextMenuItemOrSubmenu, Series, SeriesPoint} from '../MetricsGraph';
25
36
  import {useMetricsGraphDimensions} from '../MetricsGraph/useMetricsGraphDimensions';
26
37
  import {useQueryRange} from './hooks/useQueryRange';
27
38
 
39
+ const transformUtilizationLabels = (label: string, utilizationMetrics: boolean): string => {
40
+ if (utilizationMetrics) {
41
+ return label.replace('attributes.', '').replace('attributes_resource.', '');
42
+ }
43
+ return label;
44
+ };
45
+
46
+ const createProfileContextMenuItems = (
47
+ addLabelMatcher: (
48
+ labels: {key: string; value: string} | Array<{key: string; value: string}>
49
+ ) => void,
50
+ data: MetricsSeriesPb[], // The original MetricsSeriesPb[] data
51
+ utilizationMetrics = false
52
+ ): ContextMenuItemOrSubmenu[] => {
53
+ return [
54
+ {
55
+ id: 'focus-on-single-series',
56
+ label: 'Focus only on this series',
57
+ icon: 'ph:star',
58
+ onClick: (closestPoint, _series) => {
59
+ if (closestPoint != null && data.length > 0 && data[closestPoint.seriesIndex] != null) {
60
+ const originalSeriesData = data[closestPoint.seriesIndex];
61
+ if (originalSeriesData.labelset?.labels != null) {
62
+ const labels = originalSeriesData.labelset.labels.filter(
63
+ (label: Label) => label.name !== '__name__'
64
+ );
65
+ const labelsToAdd = labels.map((label: Label) => ({
66
+ key: label.name,
67
+ value: label.value,
68
+ }));
69
+ addLabelMatcher(labelsToAdd);
70
+ }
71
+ }
72
+ },
73
+ },
74
+ {
75
+ id: 'add-to-query',
76
+ label: 'Add to query',
77
+ icon: 'material-symbols:add',
78
+ createDynamicItems: (closestPoint, _series) => {
79
+ if (closestPoint == null || data.length === 0 || data[closestPoint.seriesIndex] == null) {
80
+ return [
81
+ {
82
+ id: 'no-labels-available',
83
+ label: 'No labels available',
84
+ icon: 'ph:warning',
85
+ disabled: () => true,
86
+ onClick: () => {}, // No-op for disabled item
87
+ },
88
+ ];
89
+ }
90
+
91
+ const originalSeriesData = data[closestPoint.seriesIndex];
92
+ if (originalSeriesData.labelset?.labels == null) {
93
+ return [
94
+ {
95
+ id: 'no-labels-available',
96
+ label: 'No labels available',
97
+ icon: 'ph:warning',
98
+ disabled: () => true,
99
+ onClick: () => {}, // No-op for disabled item
100
+ },
101
+ ];
102
+ }
103
+
104
+ const labels = originalSeriesData.labelset.labels.filter(
105
+ (label: Label) => label.name !== '__name__'
106
+ );
107
+
108
+ return labels.map((label: Label) => ({
109
+ id: `add-label-${label.name}`,
110
+ label: (
111
+ <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">
112
+ {`${transformUtilizationLabels(label.name, utilizationMetrics)}="${label.value}"`}
113
+ </div>
114
+ ),
115
+ onClick: () => {
116
+ addLabelMatcher({
117
+ key: label.name,
118
+ value: label.value,
119
+ });
120
+ },
121
+ }));
122
+ },
123
+ },
124
+ ];
125
+ };
126
+
127
+ const transformMetricsData = (data: MetricsSeriesPb[]): Series[] => {
128
+ const series = data.reduce<Series[]>((agg: Series[], s: MetricsSeriesPb) => {
129
+ if (s.labelset !== undefined) {
130
+ // Generate ID from sorted labelsets
131
+ const labels = s.labelset.labels ?? [];
132
+ const sortedLabels = labels
133
+ .filter(label => label.name !== '__name__') // Exclude __name__ from ID generation
134
+ .sort((a, b) => a.name.localeCompare(b.name));
135
+ const id = sortedLabels.map(label => `${label.name}=${label.value}`).join(',');
136
+
137
+ agg.push({
138
+ id: id !== '' ? id : 'default', // fallback to 'default' if no labels
139
+ values: s.samples.reduce<Array<[number, number]>>((agg, d: MetricsSample) => {
140
+ if (d.timestamp !== undefined && d.valuePerSecond !== undefined) {
141
+ const timestampMs = Number(d.timestamp.seconds) * 1000 + d.timestamp.nanos / 1_000_000;
142
+ agg.push([timestampMs, d.valuePerSecond]);
143
+ }
144
+ return agg;
145
+ }, []),
146
+ });
147
+ }
148
+ return agg;
149
+ }, []);
150
+
151
+ return series;
152
+ };
153
+
28
154
  interface ProfileMetricsEmptyStateProps {
29
155
  message: string;
30
156
  }
@@ -61,7 +187,7 @@ interface ProfileMetricsGraphProps {
61
187
  labels: {key: string; value: string} | Array<{key: string; value: string}>
62
188
  ) => void;
63
189
  onPointClick: (
64
- timestamp: number,
190
+ timestamp: bigint,
65
191
  labels: Label[],
66
192
  queryExpression: string,
67
193
  duration: number
@@ -87,7 +213,7 @@ const ProfileMetricsGraph = ({
87
213
  response,
88
214
  error,
89
215
  } = useQueryRange(queryClient, queryExpression, from, to, sumBy, sumByLoading);
90
- const {onError, perf, authenticationErrorMessage, isDarkMode} = useParcaContext();
216
+ const {onError, perf, authenticationErrorMessage, isDarkMode, timezone} = useParcaContext();
91
217
  const {width, height, margin, heightStyle} = useMetricsGraphDimensions(comparing);
92
218
 
93
219
  useEffect(() => {
@@ -104,12 +230,107 @@ const ProfileMetricsGraph = ({
104
230
  perf?.markInteraction('Metrics graph render', response.series[0].samples.length);
105
231
  }, [perf, response]);
106
232
 
107
- const series = response?.series;
233
+ const originalSeries = response?.series;
234
+
235
+ const selectedPoint = useMemo((): SeriesPoint | null => {
236
+ if (profile !== null && profile instanceof MergedProfileSelection) {
237
+ // Iterate over the series and find the series index that matches all
238
+ // labels of the profile selection. We specifically need the index
239
+ // because that's what the SeriesPoint interface expects.
240
+ const seriesIndex = originalSeries?.findIndex(s => {
241
+ return s.labelset?.labels?.every(label => {
242
+ return profile.query.matchers.some(matcher => {
243
+ return matcher.key === label.name && matcher.value === label.value;
244
+ });
245
+ });
246
+ });
247
+
248
+ // if we found a series, return the point that matches the from/to timestamp exactly (in millisecond precision)
249
+ if (
250
+ seriesIndex !== undefined &&
251
+ seriesIndex !== -1 &&
252
+ originalSeries !== undefined &&
253
+ originalSeries[seriesIndex] != null
254
+ ) {
255
+ const series = originalSeries[seriesIndex];
256
+ const pointIndex = series.samples.findIndex(sample => {
257
+ return (
258
+ sample.timestamp?.seconds === BigInt(profile.mergeFrom / 1_000_000_000n) &&
259
+ sample.timestamp?.nanos === Number(profile.mergeFrom % 1_000_000_000n)
260
+ );
261
+ });
262
+
263
+ if (pointIndex !== -1) {
264
+ return {
265
+ seriesIndex,
266
+ pointIndex,
267
+ };
268
+ }
269
+ }
270
+ return null;
271
+ }
272
+
273
+ return null;
274
+ }, [profile, originalSeries]);
108
275
 
109
- const dataAvailable = series !== null && series !== undefined && series?.length > 0;
276
+ const transformedSeries = useMemo(() => {
277
+ return originalSeries != null ? transformMetricsData(originalSeries) : [];
278
+ }, [originalSeries]);
279
+
280
+ const contextMenuItems = useMemo(() => {
281
+ return originalSeries != null
282
+ ? createProfileContextMenuItems(addLabelMatcher, originalSeries)
283
+ : [];
284
+ }, [originalSeries, addLabelMatcher]);
285
+
286
+ const dataAvailable =
287
+ originalSeries !== null && originalSeries !== undefined && originalSeries?.length > 0;
288
+
289
+ const {sampleUnit, sampleType, yAxisLabel, yAxisUnit} = useMemo(() => {
290
+ let sampleUnit = '';
291
+ let sampleType = '';
292
+
293
+ if (dataAvailable) {
294
+ if (
295
+ originalSeries?.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)
296
+ ) {
297
+ sampleUnit = originalSeries[0]?.sampleType?.unit ?? '';
298
+ sampleType = originalSeries[0]?.sampleType?.type ?? '';
299
+ }
300
+ if (sampleUnit === '') {
301
+ const profileType = Query.parse(queryExpression).profileType();
302
+ sampleUnit = profileType.sampleUnit;
303
+ sampleType = profileType.sampleType;
304
+ }
305
+ }
306
+
307
+ // Calculate axis labels based on profile data
308
+ const isDeltaType =
309
+ profile !== null ? (profile as MergedProfileSelection)?.query.profType.delta : false;
310
+ let yAxisLabel = sampleUnit;
311
+ let yAxisUnit = sampleUnit;
312
+
313
+ if (isDeltaType) {
314
+ if (sampleUnit === 'nanoseconds') {
315
+ if (sampleType === 'cpu') {
316
+ yAxisLabel = 'CPU Cores';
317
+ yAxisUnit = '';
318
+ }
319
+ if (sampleType === 'cuda') {
320
+ yAxisLabel = 'GPU Time';
321
+ }
322
+ }
323
+ if (sampleUnit === 'bytes') {
324
+ yAxisLabel = 'Bytes per Second';
325
+ }
326
+ }
327
+
328
+ return {sampleUnit, sampleType, yAxisLabel, yAxisUnit};
329
+ }, [dataAvailable, originalSeries, queryExpression, profile]);
110
330
 
111
331
  const loading = metricsGraphLoading;
112
332
 
333
+ // Handle errors after all hooks have been called
113
334
  if (!metricsGraphLoading && error !== null) {
114
335
  if (authenticationErrorMessage !== undefined && error.code === 'UNAUTHENTICATED') {
115
336
  return <ErrorContent errorMessage={authenticationErrorMessage} />;
@@ -118,21 +339,6 @@ const ProfileMetricsGraph = ({
118
339
  return <ErrorContent errorMessage={capitalizeOnlyFirstLetter(error.message)} />;
119
340
  }
120
341
 
121
- let sampleUnit = '';
122
- let sampleType = '';
123
-
124
- if (dataAvailable) {
125
- if (series.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)) {
126
- sampleUnit = series[0]?.sampleType?.unit ?? '';
127
- sampleType = series[0]?.sampleType?.type ?? '';
128
- }
129
- if (sampleUnit === '') {
130
- const profileType = Query.parse(queryExpression).profileType();
131
- sampleUnit = profileType.sampleUnit;
132
- sampleType = profileType.sampleType;
133
- }
134
- }
135
-
136
342
  return (
137
343
  <AnimatePresence>
138
344
  <motion.div
@@ -146,26 +352,213 @@ const ProfileMetricsGraph = ({
146
352
  <MetricsGraphSkeleton heightStyle={heightStyle} isDarkMode={isDarkMode} />
147
353
  ) : dataAvailable ? (
148
354
  <MetricsGraph
149
- data={series}
355
+ data={transformedSeries}
150
356
  from={from}
151
357
  to={to}
152
- profile={profile as MergedProfileSelection}
153
358
  setTimeRange={setTimeRange}
154
- onSampleClick={(
155
- timestamp: number,
156
- _value: number,
157
- labels: Label[],
158
- duration: number
159
- ): void => {
160
- onPointClick(timestamp, labels, queryExpression, duration);
359
+ selectedPoint={selectedPoint}
360
+ onSampleClick={(closestPoint: SeriesPoint): void => {
361
+ // Use original data for both series and point
362
+ if (originalSeries?.[closestPoint.seriesIndex] != null) {
363
+ const originalSeriesData = originalSeries[closestPoint.seriesIndex];
364
+ const originalPoint = originalSeriesData.samples[closestPoint.pointIndex];
365
+ if (originalPoint.timestamp != null && originalPoint.valuePerSecond !== undefined) {
366
+ const timestampNanos =
367
+ originalPoint.timestamp.seconds * 1_000_000_000n +
368
+ BigInt(originalPoint.timestamp.nanos);
369
+ onPointClick(
370
+ timestampNanos, // Convert to number to match interface
371
+ originalSeriesData.labelset?.labels ?? [],
372
+ queryExpression,
373
+ Number(originalPoint.duration ?? 0) // Convert bigint to number
374
+ );
375
+ }
376
+ }
377
+ }}
378
+ renderTooltipContent={(seriesIndex: number, pointIndex: number) => {
379
+ if (originalSeries?.[seriesIndex]?.samples?.[pointIndex] != null) {
380
+ const originalSeriesData = originalSeries[seriesIndex];
381
+ const originalPoint = originalSeriesData.samples[pointIndex];
382
+
383
+ if (originalPoint.timestamp != null && originalPoint.valuePerSecond !== undefined) {
384
+ const timestampMs =
385
+ Number(originalPoint.timestamp.seconds) * 1000 +
386
+ originalPoint.timestamp.nanos / 1_000_000;
387
+ const labels = originalSeriesData.labelset?.labels ?? [];
388
+ const nameLabel = labels.find(e => e.name === '__name__');
389
+ const highlightedNameLabel = nameLabel ?? {name: '', value: ''};
390
+
391
+ // Calculate attributes maps for utilization metrics
392
+ const utilizationMetrics = false; // This is for profile metrics, not utilization
393
+ const attributesMap = labels
394
+ .filter(
395
+ label =>
396
+ label.name.startsWith('attributes.') &&
397
+ !label.name.startsWith('attributes_resource.')
398
+ )
399
+ .reduce<Record<string, string>>((acc, label) => {
400
+ const key = label.name.replace('attributes.', '');
401
+ acc[key] = label.value;
402
+ return acc;
403
+ }, {});
404
+
405
+ const attributesResourceMap = labels
406
+ .filter(label => label.name.startsWith('attributes_resource.'))
407
+ .reduce<Record<string, string>>((acc, label) => {
408
+ const key = label.name.replace('attributes_resource.', '');
409
+ acc[key] = label.value;
410
+ return acc;
411
+ }, {});
412
+
413
+ const isDeltaType =
414
+ profile !== null
415
+ ? (profile as MergedProfileSelection)?.query.profType.delta
416
+ : false;
417
+
418
+ return (
419
+ <div className="flex flex-row">
420
+ <div className="ml-2 mr-6">
421
+ <span className="font-semibold">{highlightedNameLabel.value}</span>
422
+ <span className="my-2 block text-gray-700 dark:text-gray-300">
423
+ <table className="table-auto">
424
+ <tbody>
425
+ {isDeltaType ? (
426
+ <>
427
+ <tr>
428
+ <td className="w-1/4 pr-3">Per&nbsp;Second</td>
429
+ <td className="w-3/4">
430
+ {valueFormatter(
431
+ originalPoint.valuePerSecond,
432
+ sampleUnit === 'nanoseconds' && sampleType === 'cpu'
433
+ ? 'CPU Cores'
434
+ : sampleUnit,
435
+ 5
436
+ )}
437
+ </td>
438
+ </tr>
439
+ <tr>
440
+ <td className="w-1/4">Total</td>
441
+ <td className="w-3/4">
442
+ {valueFormatter(originalPoint.value ?? 0, sampleUnit, 2)}
443
+ </td>
444
+ </tr>
445
+ </>
446
+ ) : (
447
+ <tr>
448
+ <td className="w-1/4">Value</td>
449
+ <td className="w-3/4">
450
+ {valueFormatter(originalPoint.valuePerSecond, sampleUnit, 5)}
451
+ </td>
452
+ </tr>
453
+ )}
454
+ {originalPoint.duration != null &&
455
+ Number(originalPoint.duration) > 0 && (
456
+ <tr>
457
+ <td className="w-1/4">Duration</td>
458
+ <td className="w-3/4">
459
+ {valueFormatter(
460
+ Number(originalPoint.duration.toString()),
461
+ 'nanoseconds',
462
+ 2
463
+ )}
464
+ </td>
465
+ </tr>
466
+ )}
467
+ <tr>
468
+ <td className="w-1/4">At</td>
469
+ <td className="w-3/4">
470
+ {formatDate(
471
+ new Date(timestampMs),
472
+ timePattern(timezone as string),
473
+ timezone
474
+ )}
475
+ </td>
476
+ </tr>
477
+ </tbody>
478
+ </table>
479
+ </span>
480
+ <span className="my-2 block text-gray-500">
481
+ {utilizationMetrics ? (
482
+ <>
483
+ {Object.keys(attributesResourceMap).length > 0 && (
484
+ <span className="text-sm font-bold text-gray-700 dark:text-white">
485
+ Resource Attributes
486
+ </span>
487
+ )}
488
+ <span className="my-2 block text-gray-500">
489
+ {Object.keys(attributesResourceMap).map(name => (
490
+ <div
491
+ key={name}
492
+ 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"
493
+ >
494
+ <TextWithTooltip
495
+ text={`${name.replace('attributes.', '')}="${
496
+ attributesResourceMap[name]
497
+ }"`}
498
+ maxTextLength={48}
499
+ id={`tooltip-${name}-${attributesResourceMap[name]}`}
500
+ />
501
+ </div>
502
+ ))}
503
+ </span>
504
+ {Object.keys(attributesMap).length > 0 && (
505
+ <span className="text-sm font-bold text-gray-700 dark:text-white">
506
+ Attributes
507
+ </span>
508
+ )}
509
+ <span className="my-2 block text-gray-500">
510
+ {Object.keys(attributesMap).map(name => (
511
+ <div
512
+ key={name}
513
+ 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"
514
+ >
515
+ <TextWithTooltip
516
+ text={`${name.replace('attributes.', '')}="${
517
+ attributesMap[name]
518
+ }"`}
519
+ maxTextLength={48}
520
+ id={`tooltip-${name}-${attributesMap[name]}`}
521
+ />
522
+ </div>
523
+ ))}
524
+ </span>
525
+ </>
526
+ ) : (
527
+ <>
528
+ {labels
529
+ .filter((label: Label) => label.name !== '__name__')
530
+ .map((label: Label) => (
531
+ <div
532
+ key={label.name}
533
+ 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"
534
+ >
535
+ <TextWithTooltip
536
+ text={`${label.name}="${label.value}"`}
537
+ maxTextLength={37}
538
+ id={`tooltip-${label.name}`}
539
+ />
540
+ </div>
541
+ ))}
542
+ </>
543
+ )}
544
+ </span>
545
+ <div className="flex w-full items-center gap-1 text-xs text-gray-500">
546
+ <Icon icon="iconoir:mouse-button-right" />
547
+ <div>Right click to add labels to query.</div>
548
+ </div>
549
+ </div>
550
+ </div>
551
+ );
552
+ }
553
+ }
554
+ return null;
161
555
  }}
162
- addLabelMatcher={addLabelMatcher}
163
- sampleUnit={sampleUnit}
164
- sampleType={sampleType}
556
+ yAxisLabel={yAxisLabel}
557
+ yAxisUnit={yAxisUnit}
165
558
  height={height}
166
559
  width={width}
167
560
  margin={margin}
168
- sumBy={sumBy}
561
+ contextMenuItems={contextMenuItems}
169
562
  />
170
563
  ) : (
171
564
  <ProfileMetricsEmptyState message="No data found. Try a different query." />
@@ -47,7 +47,7 @@ interface MetricsGraphSectionProps {
47
47
  data: UtilizationMetricsType[];
48
48
  }>;
49
49
  utilizationMetricsLoading?: boolean;
50
- onUtilizationSeriesSelect?: (series: Array<{key: string; value: string}>) => void;
50
+ onUtilizationSeriesSelect?: (seriesIndex: number) => void;
51
51
  }
52
52
 
53
53
  export function MetricsGraphSection({
@@ -123,7 +123,7 @@ export function MetricsGraphSection({
123
123
  };
124
124
 
125
125
  const handlePointClick = (
126
- timestamp: number,
126
+ timestamp: bigint,
127
127
  labels: Label[],
128
128
  queryExpression: string,
129
129
  duration: number
@@ -136,9 +136,8 @@ export function MetricsGraphSection({
136
136
  }
137
137
  });
138
138
 
139
- const durationInMilliseconds = duration / 1000000; // duration is in nanoseconds
140
139
  const mergeFrom = timestamp;
141
- const mergeTo = query.profileType().delta ? mergeFrom + durationInMilliseconds : mergeFrom;
140
+ const mergeTo = query.profileType().delta ? mergeFrom + BigInt(duration) : mergeFrom;
142
141
 
143
142
  resetStateOnSeriesChange(); // reset some state when a new series is selected
144
143
  selectProfile(new MergedProfileSelection(mergeFrom, mergeTo, query));
@@ -175,14 +174,19 @@ export function MetricsGraphSection({
175
174
  <UtilizationMetricsGraph
176
175
  key={name}
177
176
  data={data}
178
- addLabelMatcher={addLabelMatcher}
179
177
  setTimeRange={handleTimeRangeChange}
180
178
  utilizationMetricsLoading={utilizationMetricsLoading}
181
- name={name}
182
179
  humanReadableName={humanReadableName}
183
180
  from={querySelection.from}
184
181
  to={querySelection.to}
185
- onSelectedSeriesChange={onUtilizationSeriesSelect}
182
+ yAxisUnit="percentage"
183
+ addLabelMatcher={addLabelMatcher}
184
+ onSeriesClick={seriesIndex => {
185
+ // For generic UtilizationMetrics, just pass the series index
186
+ if (onUtilizationSeriesSelect != null) {
187
+ onUtilizationSeriesSelect(seriesIndex);
188
+ }
189
+ }}
186
190
  />
187
191
  </>
188
192
  );
@@ -207,7 +211,7 @@ export function MetricsGraphSection({
207
211
  to={querySelection.to}
208
212
  utilizationMetricsLoading={utilizationMetricsLoading}
209
213
  selectedSeries={undefined}
210
- onSelectedSeriesChange={onUtilizationSeriesSelect}
214
+ onSeriesClick={onUtilizationSeriesSelect}
211
215
  />
212
216
  )}
213
217
  </div>
@@ -42,8 +42,8 @@ export interface QuerySelection {
42
42
  to: number;
43
43
  timeSelection: string;
44
44
  sumBy?: string[];
45
- mergeFrom?: number;
46
- mergeTo?: number;
45
+ mergeFrom?: string;
46
+ mergeTo?: string;
47
47
  }
48
48
 
49
49
  interface ProfileSelectorFeatures {
@@ -94,7 +94,7 @@ interface ProfileSelectorProps extends ProfileSelectorFeatures {
94
94
  }>;
95
95
  utilizationMetricsLoading?: boolean;
96
96
  utilizationLabels?: UtilizationLabels;
97
- onUtilizationSeriesSelect?: (series: Array<{key: string; value: string}>) => void;
97
+ onUtilizationSeriesSelect?: (seriesIndex: number) => void;
98
98
  }
99
99
 
100
100
  export interface IProfileTypesResult {
@@ -237,8 +237,8 @@ const ProfileSelector = ({
237
237
  const to = timeRangeSelection.getToMs(updateTs);
238
238
  const mergeParams = delta
239
239
  ? {
240
- mergeFrom: from,
241
- mergeTo: to,
240
+ mergeFrom: (BigInt(from) * 1_000_000n).toString(),
241
+ mergeTo: (BigInt(to) * 1_000_000n).toString(),
242
242
  }
243
243
  : {};
244
244