@parca/profile 0.19.82 → 0.19.83

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 (83) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/MatchersInput/index.d.ts +34 -2
  3. package/dist/MatchersInput/index.d.ts.map +1 -1
  4. package/dist/MatchersInput/index.js +91 -14
  5. package/dist/MetricsGraph/UtilizationMetrics/Throughput.d.ts +29 -0
  6. package/dist/MetricsGraph/UtilizationMetrics/Throughput.d.ts.map +1 -0
  7. package/dist/MetricsGraph/UtilizationMetrics/Throughput.js +175 -0
  8. package/dist/MetricsGraph/UtilizationMetrics/index.d.ts +28 -0
  9. package/dist/MetricsGraph/UtilizationMetrics/index.d.ts.map +1 -0
  10. package/dist/MetricsGraph/UtilizationMetrics/index.js +186 -0
  11. package/dist/MetricsGraph/index.d.ts.map +1 -1
  12. package/dist/MetricsGraph/index.js +1 -13
  13. package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
  14. package/dist/ProfileMetricsGraph/index.js +29 -6
  15. package/dist/ProfileSelector/MetricsGraphSection.d.ts +9 -2
  16. package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
  17. package/dist/ProfileSelector/MetricsGraphSection.js +38 -3
  18. package/dist/ProfileSelector/QueryControls.d.ts +43 -0
  19. package/dist/ProfileSelector/QueryControls.d.ts.map +1 -0
  20. package/dist/{QueryControls/index.js → ProfileSelector/QueryControls.js} +13 -16
  21. package/dist/ProfileSelector/index.d.ts +29 -1
  22. package/dist/ProfileSelector/index.d.ts.map +1 -1
  23. package/dist/ProfileSelector/index.js +9 -12
  24. package/dist/SimpleMatchers/Select.js +1 -1
  25. package/dist/SimpleMatchers/index.d.ts +11 -2
  26. package/dist/SimpleMatchers/index.d.ts.map +1 -1
  27. package/dist/SimpleMatchers/index.js +45 -34
  28. package/dist/ViewMatchers/index.d.ts +9 -0
  29. package/dist/ViewMatchers/index.d.ts.map +1 -1
  30. package/dist/ViewMatchers/index.js +12 -20
  31. package/dist/contexts/MatchersInputLabelsContext.d.ts +29 -0
  32. package/dist/contexts/MatchersInputLabelsContext.d.ts.map +1 -0
  33. package/dist/contexts/MatchersInputLabelsContext.js +79 -0
  34. package/dist/contexts/SimpleMatchersLabelContext.d.ts +25 -0
  35. package/dist/contexts/SimpleMatchersLabelContext.d.ts.map +1 -0
  36. package/dist/contexts/SimpleMatchersLabelContext.js +115 -0
  37. package/dist/contexts/UtilizationLabelsContext.d.ts +15 -0
  38. package/dist/contexts/UtilizationLabelsContext.d.ts.map +1 -0
  39. package/dist/contexts/UtilizationLabelsContext.js +25 -0
  40. package/dist/hooks/useQueryState.d.ts +0 -2
  41. package/dist/hooks/useQueryState.d.ts.map +1 -1
  42. package/dist/hooks/useQueryState.js +0 -18
  43. package/dist/index.d.ts +3 -9
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +3 -9
  46. package/dist/styles.css +1 -1
  47. package/dist/useSumBy.js +1 -1
  48. package/package.json +2 -2
  49. package/src/MatchersInput/index.tsx +163 -17
  50. package/src/MetricsGraph/UtilizationMetrics/Throughput.tsx +405 -0
  51. package/src/MetricsGraph/UtilizationMetrics/index.tsx +426 -0
  52. package/src/MetricsGraph/index.tsx +1 -17
  53. package/src/ProfileMetricsGraph/index.tsx +98 -17
  54. package/src/ProfileSelector/MetricsGraphSection.tsx +115 -14
  55. package/src/{QueryControls/index.tsx → ProfileSelector/QueryControls.tsx} +84 -66
  56. package/src/ProfileSelector/index.tsx +109 -106
  57. package/src/SimpleMatchers/Select.tsx +1 -1
  58. package/src/SimpleMatchers/index.tsx +85 -46
  59. package/src/ViewMatchers/index.tsx +30 -22
  60. package/src/contexts/MatchersInputLabelsContext.tsx +141 -0
  61. package/src/contexts/SimpleMatchersLabelContext.tsx +189 -0
  62. package/src/contexts/UtilizationLabelsContext.tsx +45 -0
  63. package/src/hooks/useQueryState.ts +0 -25
  64. package/src/index.tsx +3 -29
  65. package/src/useSumBy.ts +1 -1
  66. package/dist/QueryControls/index.d.ts +0 -42
  67. package/dist/QueryControls/index.d.ts.map +0 -1
  68. package/dist/contexts/LabelsQueryProvider.d.ts +0 -35
  69. package/dist/contexts/LabelsQueryProvider.d.ts.map +0 -1
  70. package/dist/contexts/LabelsQueryProvider.js +0 -70
  71. package/dist/contexts/UnifiedLabelsContext.d.ts +0 -37
  72. package/dist/contexts/UnifiedLabelsContext.d.ts.map +0 -1
  73. package/dist/contexts/UnifiedLabelsContext.js +0 -88
  74. package/dist/contexts/utils.d.ts +0 -10
  75. package/dist/contexts/utils.d.ts.map +0 -1
  76. package/dist/contexts/utils.js +0 -31
  77. package/dist/hooks/useLabels.d.ts +0 -23
  78. package/dist/hooks/useLabels.d.ts.map +0 -1
  79. package/dist/hooks/useLabels.js +0 -75
  80. package/src/contexts/LabelsQueryProvider.tsx +0 -142
  81. package/src/contexts/UnifiedLabelsContext.tsx +0 -155
  82. package/src/contexts/utils.ts +0 -43
  83. package/src/hooks/useLabels.ts +0 -121
@@ -0,0 +1,426 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import {useMemo} from 'react';
15
+
16
+ import {Icon} from '@iconify/react';
17
+ import {AnimatePresence, motion} from 'framer-motion';
18
+
19
+ import {
20
+ DateTimeRange,
21
+ MetricsGraphSkeleton,
22
+ TextWithTooltip,
23
+ useParcaContext,
24
+ } from '@parca/components';
25
+ import {formatDate, timePattern, valueFormatter} from '@parca/utilities';
26
+
27
+ import {type UtilizationMetrics as MetricSeries} from '../../ProfileSelector';
28
+ import MetricsGraph, {type ContextMenuItemOrSubmenu, type Series} from '../index';
29
+ import {useMetricsGraphDimensions} from '../useMetricsGraphDimensions';
30
+
31
+ interface CommonProps {
32
+ setTimeRange: (range: DateTimeRange) => void;
33
+ humanReadableName: string;
34
+ from: number;
35
+ to: number;
36
+ onSeriesClick?: (seriesIndex: number) => void;
37
+ }
38
+
39
+ type RawUtilizationMetricsProps = CommonProps & {
40
+ data: Series[];
41
+ originalData: MetricSeries[];
42
+ width: number;
43
+ height: number;
44
+ margin: number;
45
+ yAxisUnit: string;
46
+ contextMenuItems?: ContextMenuItemOrSubmenu[];
47
+ };
48
+
49
+ type Props = CommonProps & {
50
+ data: MetricSeries[];
51
+ yAxisUnit: string;
52
+ utilizationMetricsLoading?: boolean;
53
+ addLabelMatcher?: (
54
+ labels: {key: string; value: string} | Array<{key: string; value: string}>
55
+ ) => void;
56
+ onSelectedSeriesChange?: (series: Array<{key: string; value: string}>) => void;
57
+ };
58
+
59
+ const transformUtilizationLabels = (label: string): string => {
60
+ return label.replace('attributes.', '').replace('attributes_resource.', '');
61
+ };
62
+
63
+ const createUtilizationContextMenuItems = (
64
+ addLabelMatcher: (
65
+ labels: {key: string; value: string} | Array<{key: string; value: string}>
66
+ ) => void,
67
+ originalData: MetricSeries[]
68
+ ): ContextMenuItemOrSubmenu[] => {
69
+ return [
70
+ {
71
+ id: 'focus-on-single-series',
72
+ label: 'Focus only on this series',
73
+ icon: 'ph:star',
74
+ onClick: (closestPoint, _series) => {
75
+ if (
76
+ closestPoint != null &&
77
+ originalData.length > 0 &&
78
+ originalData[closestPoint.seriesIndex] != null
79
+ ) {
80
+ const originalSeriesData = originalData[closestPoint.seriesIndex];
81
+ if (originalSeriesData.labelset?.labels != null) {
82
+ const labels = originalSeriesData.labelset.labels.filter(
83
+ label => label.name !== '__name__'
84
+ );
85
+ const labelsToAdd = labels.map(label => ({
86
+ key: label.name,
87
+ value: label.value,
88
+ }));
89
+ addLabelMatcher(labelsToAdd);
90
+ }
91
+ }
92
+ },
93
+ },
94
+ {
95
+ id: 'add-to-query',
96
+ label: 'Add to query',
97
+ icon: 'material-symbols:add',
98
+ createDynamicItems: (closestPoint, _series) => {
99
+ if (
100
+ closestPoint == null ||
101
+ originalData.length === 0 ||
102
+ originalData[closestPoint.seriesIndex] == null
103
+ ) {
104
+ return [
105
+ {
106
+ id: 'no-labels-available',
107
+ label: 'No labels available',
108
+ icon: 'ph:warning',
109
+ disabled: () => true,
110
+ onClick: () => {}, // No-op for disabled item
111
+ },
112
+ ];
113
+ }
114
+
115
+ const originalSeriesData = originalData[closestPoint.seriesIndex];
116
+ if (originalSeriesData.labelset?.labels == null) {
117
+ return [
118
+ {
119
+ id: 'no-labels-available',
120
+ label: 'No labels available',
121
+ icon: 'ph:warning',
122
+ disabled: () => true,
123
+ onClick: () => {}, // No-op for disabled item
124
+ },
125
+ ];
126
+ }
127
+
128
+ const labels = originalSeriesData.labelset.labels.filter(
129
+ label => label.name !== '__name__'
130
+ );
131
+
132
+ return labels.map(label => ({
133
+ id: `add-label-${label.name}`,
134
+ label: (
135
+ <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">
136
+ {`${transformUtilizationLabels(label.name)}="${label.value}"`}
137
+ </div>
138
+ ),
139
+ onClick: () => {
140
+ addLabelMatcher({
141
+ key: label.name,
142
+ value: label.value,
143
+ });
144
+ },
145
+ }));
146
+ },
147
+ },
148
+ ];
149
+ };
150
+
151
+ const transformMetricSeriesToSeries = (data: MetricSeries[]): Series[] => {
152
+ return data.map(metricSeries => {
153
+ if (metricSeries.labelset != null) {
154
+ const labels = metricSeries.labelset.labels ?? [];
155
+ const sortedLabels = labels.sort((a, b) => a.name.localeCompare(b.name));
156
+ const id = sortedLabels.map(label => `${label.name}=${label.value}`).join(',');
157
+
158
+ return {
159
+ id: id !== '' ? id : 'default',
160
+ values: metricSeries.samples.map((sample): [number, number] => [
161
+ sample.timestamp,
162
+ sample.value,
163
+ ]),
164
+ };
165
+ }
166
+ return {
167
+ id: 'default',
168
+ values: [],
169
+ };
170
+ });
171
+ };
172
+
173
+ const _getYAxisUnit = (name: string): string => {
174
+ switch (name) {
175
+ case 'gpu_power_watt':
176
+ return 'watts';
177
+ case 'gpu_temperature_celsius':
178
+ return 'celsius';
179
+ case 'gpu_clock_hertz':
180
+ return 'hertz';
181
+ default:
182
+ return 'percent';
183
+ }
184
+ };
185
+
186
+ const RawUtilizationMetrics = ({
187
+ data,
188
+ originalData,
189
+ setTimeRange,
190
+ width,
191
+ height,
192
+ margin,
193
+ humanReadableName,
194
+ from,
195
+ to,
196
+ yAxisUnit,
197
+ contextMenuItems,
198
+ onSeriesClick,
199
+ }: RawUtilizationMetricsProps): JSX.Element => {
200
+ const {timezone} = useParcaContext();
201
+
202
+ return (
203
+ <MetricsGraph
204
+ data={data.map((val, idx) => ({
205
+ ...val,
206
+ highlighted: originalData?.[idx]?.isSelected ?? false,
207
+ }))}
208
+ from={from}
209
+ to={to}
210
+ setTimeRange={setTimeRange}
211
+ onSampleClick={closestPoint => {
212
+ if (onSeriesClick != null) {
213
+ onSeriesClick(closestPoint.seriesIndex);
214
+ }
215
+ }}
216
+ yAxisLabel={humanReadableName}
217
+ yAxisUnit={yAxisUnit}
218
+ width={width}
219
+ height={height}
220
+ margin={margin}
221
+ contextMenuItems={contextMenuItems}
222
+ renderTooltipContent={(seriesIndex: number, pointIndex: number) => {
223
+ if (originalData?.[seriesIndex]?.samples?.[pointIndex] != null) {
224
+ const originalSeriesData = originalData[seriesIndex];
225
+ const originalPoint = originalData[seriesIndex].samples[pointIndex];
226
+
227
+ const labels = originalSeriesData.labelset?.labels ?? [];
228
+ const nameLabel = labels.find(e => e.name === '__name__');
229
+ const highlightedNameLabel = nameLabel ?? {name: '', value: ''};
230
+
231
+ // Calculate attributes maps for utilization metrics
232
+ const attributesMap = labels
233
+ .filter(
234
+ label =>
235
+ label.name.startsWith('attributes.') &&
236
+ !label.name.startsWith('attributes_resource.')
237
+ )
238
+ .reduce<Record<string, string>>((acc, label) => {
239
+ const key = label.name.replace('attributes.', '');
240
+ acc[key] = label.value;
241
+ return acc;
242
+ }, {});
243
+
244
+ const attributesResourceMap = labels
245
+ .filter(label => label.name.startsWith('attributes_resource.'))
246
+ .reduce<Record<string, string>>((acc, label) => {
247
+ const key = label.name.replace('attributes_resource.', '');
248
+ acc[key] = label.value;
249
+ return acc;
250
+ }, {});
251
+
252
+ return (
253
+ <div className="flex flex-row">
254
+ <div className="ml-2 mr-6">
255
+ <span className="font-semibold">{highlightedNameLabel.value}</span>
256
+ <span className="my-2 block text-gray-700 dark:text-gray-300">
257
+ <table className="table-auto">
258
+ <tbody>
259
+ <tr>
260
+ <td className="w-1/4">Value</td>
261
+ <td className="w-3/4">
262
+ {valueFormatter(originalPoint.value, yAxisUnit, 2)}
263
+ </td>
264
+ </tr>
265
+ <tr>
266
+ <td className="w-1/4">At</td>
267
+ <td className="w-3/4">
268
+ {formatDate(
269
+ new Date(originalPoint.timestamp),
270
+ timePattern(timezone as string),
271
+ timezone
272
+ )}
273
+ </td>
274
+ </tr>
275
+ </tbody>
276
+ </table>
277
+ </span>
278
+ <span className="my-2 block text-gray-500">
279
+ {Object.keys(attributesResourceMap).length > 0 ? (
280
+ <span className="text-sm font-bold text-gray-700 dark:text-white">
281
+ Resource Attributes
282
+ </span>
283
+ ) : null}
284
+ <span className="my-2 block text-gray-500">
285
+ {Object.keys(attributesResourceMap).map(name => (
286
+ <div
287
+ key={
288
+ 'resourceattribute-' +
289
+ seriesIndex.toString() +
290
+ '-' +
291
+ pointIndex.toString() +
292
+ '-' +
293
+ name
294
+ }
295
+ 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"
296
+ >
297
+ <TextWithTooltip
298
+ text={`${transformUtilizationLabels(name)}="${
299
+ attributesResourceMap[name] ?? ''
300
+ }"`}
301
+ maxTextLength={48}
302
+ id={`tooltip-${name}-${attributesResourceMap[name] ?? ''}`}
303
+ />
304
+ </div>
305
+ ))}
306
+ </span>
307
+ {Object.keys(attributesMap).length > 0 ? (
308
+ <span className="text-sm font-bold text-gray-700 dark:text-white">
309
+ Attributes
310
+ </span>
311
+ ) : null}
312
+ <span className="my-2 block text-gray-500">
313
+ {Object.keys(attributesMap).map(name => (
314
+ <div
315
+ key={
316
+ 'attribute-' +
317
+ seriesIndex.toString() +
318
+ '-' +
319
+ pointIndex.toString() +
320
+ '-' +
321
+ name
322
+ }
323
+ 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"
324
+ >
325
+ <TextWithTooltip
326
+ text={`${transformUtilizationLabels(name)}="${
327
+ attributesMap[name] ?? ''
328
+ }"`}
329
+ maxTextLength={48}
330
+ id={`tooltip-${name}-${attributesMap[name] ?? ''}`}
331
+ />
332
+ </div>
333
+ ))}
334
+ </span>
335
+ {labels
336
+ .filter(
337
+ label => label.name !== '__name__' && !label.name.startsWith('attributes')
338
+ )
339
+ .map(label => (
340
+ <div
341
+ key={
342
+ 'attribute-' +
343
+ seriesIndex.toString() +
344
+ '-' +
345
+ pointIndex.toString() +
346
+ '-label-' +
347
+ label.name
348
+ }
349
+ 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"
350
+ >
351
+ <TextWithTooltip
352
+ text={`${transformUtilizationLabels(label.name)}="${label.value}"`}
353
+ maxTextLength={37}
354
+ id={`tooltip-${label.name}`}
355
+ />
356
+ </div>
357
+ ))}
358
+ </span>
359
+ <div className="flex w-full items-center gap-1 text-xs text-gray-500">
360
+ <Icon icon="iconoir:mouse-button-right" />
361
+ <div>Right click to add labels to query.</div>
362
+ </div>
363
+ </div>
364
+ </div>
365
+ );
366
+ }
367
+ return null;
368
+ }}
369
+ />
370
+ );
371
+ };
372
+
373
+ const UtilizationMetrics = ({
374
+ data,
375
+ setTimeRange,
376
+ utilizationMetricsLoading,
377
+ humanReadableName,
378
+ from,
379
+ to,
380
+ yAxisUnit,
381
+ addLabelMatcher,
382
+ onSeriesClick,
383
+ onSelectedSeriesChange: _onSelectedSeriesChange,
384
+ }: Props): JSX.Element => {
385
+ const {isDarkMode} = useParcaContext();
386
+ const {width, height, margin, heightStyle} = useMetricsGraphDimensions(false, true);
387
+
388
+ const transformedData = useMemo(() => transformMetricSeriesToSeries(data), [data]);
389
+
390
+ const contextMenuItems = useMemo(() => {
391
+ return addLabelMatcher != null ? createUtilizationContextMenuItems(addLabelMatcher, data) : [];
392
+ }, [addLabelMatcher, data]);
393
+
394
+ return (
395
+ <AnimatePresence>
396
+ <motion.div
397
+ className="w-full relative"
398
+ key="utilization-metrics-graph-loaded"
399
+ initial={false}
400
+ animate={{display: 'block', opacity: 1}}
401
+ transition={{duration: 0.5}}
402
+ >
403
+ {utilizationMetricsLoading === true ? (
404
+ <MetricsGraphSkeleton heightStyle={heightStyle} isDarkMode={isDarkMode} isMini={true} />
405
+ ) : (
406
+ <RawUtilizationMetrics
407
+ data={transformedData}
408
+ originalData={data}
409
+ setTimeRange={setTimeRange}
410
+ width={width}
411
+ height={height}
412
+ margin={margin}
413
+ humanReadableName={humanReadableName}
414
+ from={from}
415
+ to={to}
416
+ yAxisUnit={yAxisUnit}
417
+ contextMenuItems={contextMenuItems}
418
+ onSeriesClick={onSeriesClick}
419
+ />
420
+ )}
421
+ </motion.div>
422
+ </AnimatePresence>
423
+ );
424
+ };
425
+
426
+ export default UtilizationMetrics;
@@ -114,7 +114,6 @@ const MetricsGraph = ({
114
114
  };
115
115
 
116
116
  export default MetricsGraph;
117
-
118
117
  export type {ContextMenuItemOrSubmenu, ContextMenuItem, ContextMenuSubmenu};
119
118
 
120
119
  export const parseValue = (value: string): number | null => {
@@ -208,13 +207,6 @@ export const RawMetricsGraph = ({
208
207
  }
209
208
 
210
209
  const closestPointPerSeries = series.map(function (s) {
211
- if (s.values.length === 0) {
212
- return {
213
- pointIndex: undefined,
214
- distance: Infinity,
215
- };
216
- }
217
-
218
210
  const distances = s.values.map(d => {
219
211
  const x = xScale(d[0]) + margin / 2; // d[0] is timestamp_ms
220
212
  const y = yScale(d[1]) - margin / 3; // d[1] is value
@@ -224,7 +216,7 @@ export const RawMetricsGraph = ({
224
216
  });
225
217
 
226
218
  const pointIndex = d3.minIndex(distances);
227
- const minDistance = pointIndex != null ? distances[pointIndex] : Infinity;
219
+ const minDistance = distances[pointIndex];
228
220
 
229
221
  return {
230
222
  pointIndex,
@@ -233,15 +225,7 @@ export const RawMetricsGraph = ({
233
225
  });
234
226
 
235
227
  const closestSeriesIndex = d3.minIndex(closestPointPerSeries, s => s.distance);
236
- if (closestSeriesIndex == null || closestPointPerSeries[closestSeriesIndex] == null) {
237
- return null;
238
- }
239
-
240
228
  const pointIndex = closestPointPerSeries[closestSeriesIndex].pointIndex;
241
- if (pointIndex == null) {
242
- return null;
243
- }
244
-
245
229
  return {
246
230
  seriesIndex: closestSeriesIndex,
247
231
  pointIndex,
@@ -37,11 +37,19 @@ import MetricsGraph, {ContextMenuItemOrSubmenu, Series, SeriesPoint} from '../Me
37
37
  import {useMetricsGraphDimensions} from '../MetricsGraph/useMetricsGraphDimensions';
38
38
  import {useQueryRange} from './hooks/useQueryRange';
39
39
 
40
+ const transformUtilizationLabels = (label: string, utilizationMetrics: boolean): string => {
41
+ if (utilizationMetrics) {
42
+ return label.replace('attributes.', '').replace('attributes_resource.', '');
43
+ }
44
+ return label;
45
+ };
46
+
40
47
  const createProfileContextMenuItems = (
41
48
  addLabelMatcher: (
42
49
  labels: {key: string; value: string} | Array<{key: string; value: string}>
43
50
  ) => void,
44
- data: MetricsSeriesPb[] // The original MetricsSeriesPb[] data
51
+ data: MetricsSeriesPb[], // The original MetricsSeriesPb[] data
52
+ utilizationMetrics = false
45
53
  ): ContextMenuItemOrSubmenu[] => {
46
54
  return [
47
55
  {
@@ -99,7 +107,7 @@ const createProfileContextMenuItems = (
99
107
  id: `add-label-${label.name}`,
100
108
  label: (
101
109
  <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">
102
- {`${label.name}="${label.value}"`}
110
+ {`${transformUtilizationLabels(label.name, utilizationMetrics)}="${label.value}"`}
103
111
  </div>
104
112
  ),
105
113
  onClick: () => {
@@ -430,6 +438,28 @@ const ProfileMetricsGraph = ({
430
438
  const nameLabel = labels.find(e => e.name === '__name__');
431
439
  const highlightedNameLabel = nameLabel ?? {name: '', value: ''};
432
440
 
441
+ // Calculate attributes maps for utilization metrics
442
+ const utilizationMetrics = false; // This is for profile metrics, not utilization
443
+ const attributesMap = labels
444
+ .filter(
445
+ label =>
446
+ label.name.startsWith('attributes.') &&
447
+ !label.name.startsWith('attributes_resource.')
448
+ )
449
+ .reduce<Record<string, string>>((acc, label) => {
450
+ const key = label.name.replace('attributes.', '');
451
+ acc[key] = label.value;
452
+ return acc;
453
+ }, {});
454
+
455
+ const attributesResourceMap = labels
456
+ .filter(label => label.name.startsWith('attributes_resource.'))
457
+ .reduce<Record<string, string>>((acc, label) => {
458
+ const key = label.name.replace('attributes_resource.', '');
459
+ acc[key] = label.value;
460
+ return acc;
461
+ }, {});
462
+
433
463
  const isDeltaType =
434
464
  profile !== null
435
465
  ? (profile as MergedProfileSelection)?.query.profType.delta
@@ -498,21 +528,72 @@ const ProfileMetricsGraph = ({
498
528
  </table>
499
529
  </span>
500
530
  <span className="my-2 block text-gray-500">
501
- {labels
502
- .filter((label: Label) => label.name !== '__name__')
503
- .map((label: Label) => (
504
- <div
505
- key={label.name}
506
- 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"
507
- {...testId(TEST_IDS.TOOLTIP_LABEL)}
508
- >
509
- <TextWithTooltip
510
- text={`${label.name}="${label.value}"`}
511
- maxTextLength={37}
512
- id={`tooltip-${label.name}`}
513
- />
514
- </div>
515
- ))}
531
+ {utilizationMetrics ? (
532
+ <>
533
+ {Object.keys(attributesResourceMap).length > 0 && (
534
+ <span className="text-sm font-bold text-gray-700 dark:text-white">
535
+ Resource Attributes
536
+ </span>
537
+ )}
538
+ <span className="my-2 block text-gray-500">
539
+ {Object.keys(attributesResourceMap).map(name => (
540
+ <div
541
+ key={name}
542
+ 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"
543
+ {...testId(TEST_IDS.TOOLTIP_LABEL)}
544
+ >
545
+ <TextWithTooltip
546
+ text={`${name.replace('attributes.', '')}="${
547
+ attributesResourceMap[name]
548
+ }"`}
549
+ maxTextLength={48}
550
+ id={`tooltip-${name}-${attributesResourceMap[name]}`}
551
+ />
552
+ </div>
553
+ ))}
554
+ </span>
555
+ {Object.keys(attributesMap).length > 0 && (
556
+ <span className="text-sm font-bold text-gray-700 dark:text-white">
557
+ Attributes
558
+ </span>
559
+ )}
560
+ <span className="my-2 block text-gray-500">
561
+ {Object.keys(attributesMap).map(name => (
562
+ <div
563
+ key={name}
564
+ 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"
565
+ {...testId(TEST_IDS.TOOLTIP_LABEL)}
566
+ >
567
+ <TextWithTooltip
568
+ text={`${name.replace('attributes.', '')}="${
569
+ attributesMap[name]
570
+ }"`}
571
+ maxTextLength={48}
572
+ id={`tooltip-${name}-${attributesMap[name]}`}
573
+ />
574
+ </div>
575
+ ))}
576
+ </span>
577
+ </>
578
+ ) : (
579
+ <>
580
+ {labels
581
+ .filter((label: Label) => label.name !== '__name__')
582
+ .map((label: Label) => (
583
+ <div
584
+ key={label.name}
585
+ 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"
586
+ {...testId(TEST_IDS.TOOLTIP_LABEL)}
587
+ >
588
+ <TextWithTooltip
589
+ text={`${label.name}="${label.value}"`}
590
+ maxTextLength={37}
591
+ id={`tooltip-${label.name}`}
592
+ />
593
+ </div>
594
+ ))}
595
+ </>
596
+ )}
516
597
  </span>
517
598
  <div className="flex w-full items-center gap-1 text-xs text-gray-500">
518
599
  <Icon icon="iconoir:mouse-button-right" />