@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,85 +11,166 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {Fragment, useCallback, useId, useMemo, useRef, useState} from 'react';
14
+ import {useMemo} from 'react';
15
15
 
16
- import * as d3 from 'd3';
17
- import {pointer} from 'd3-selection';
16
+ import {Icon} from '@iconify/react';
18
17
  import {AnimatePresence, motion} from 'framer-motion';
19
- import throttle from 'lodash.throttle';
20
- import {useContextMenu} from 'react-contexify';
21
18
 
22
- import {DateTimeRange, MetricsGraphSkeleton, useParcaContext, useURLState} from '@parca/components';
23
- import {formatDate, formatForTimespan, getPrecision, valueFormatter} from '@parca/utilities';
19
+ import {
20
+ DateTimeRange,
21
+ MetricsGraphSkeleton,
22
+ TextWithTooltip,
23
+ useParcaContext,
24
+ } from '@parca/components';
25
+ import {formatDate, timePattern, valueFormatter} from '@parca/utilities';
24
26
 
25
- import MetricsSeries from '../../MetricsSeries';
26
27
  import {type UtilizationMetrics as MetricSeries} from '../../ProfileSelector';
27
- import MetricsContextMenu from '../MetricsContextMenu';
28
- import MetricsTooltip from '../MetricsTooltip';
29
- import {type Series} from '../index';
28
+ import MetricsGraph, {type ContextMenuItemOrSubmenu, type Series} from '../index';
30
29
  import {useMetricsGraphDimensions} from '../useMetricsGraphDimensions';
31
- import {getSeriesColor} from '../utils/colorMapping';
32
30
 
33
31
  interface CommonProps {
34
- data: MetricSeries[];
35
- addLabelMatcher: (
36
- labels: {key: string; value: string} | Array<{key: string; value: string}>
37
- ) => void;
38
32
  setTimeRange: (range: DateTimeRange) => void;
39
- name: string;
40
33
  humanReadableName: string;
41
34
  from: number;
42
35
  to: number;
43
- onSelectedSeriesChange?: (series: Array<{key: string; value: string}>) => void;
36
+ onSeriesClick?: (seriesIndex: number) => void;
44
37
  }
45
38
 
46
39
  type RawUtilizationMetricsProps = CommonProps & {
40
+ data: Series[];
41
+ originalData: MetricSeries[];
47
42
  width: number;
48
43
  height: number;
49
44
  margin: number;
45
+ yAxisUnit: string;
46
+ contextMenuItems?: ContextMenuItemOrSubmenu[];
50
47
  };
51
48
 
52
49
  type Props = CommonProps & {
53
50
  data: MetricSeries[];
54
- addLabelMatcher: (
51
+ yAxisUnit: string;
52
+ utilizationMetricsLoading?: boolean;
53
+ addLabelMatcher?: (
55
54
  labels: {key: string; value: string} | Array<{key: string; value: string}>
56
55
  ) => void;
57
- setTimeRange: (range: DateTimeRange) => void;
58
- utilizationMetricsLoading?: boolean;
56
+ onSelectedSeriesChange?: (series: Array<{key: string; value: string}>) => void;
59
57
  };
60
58
 
61
- interface MetricsSample {
62
- timestamp: number;
63
- value: number;
64
- }
59
+ const transformUtilizationLabels = (label: string): string => {
60
+ return label.replace('attributes.', '').replace('attributes_resource.', '');
61
+ };
65
62
 
66
- function transformToSeries(data: MetricSeries[]): Series[] {
67
- const series: Series[] = data.reduce<Series[]>(function (agg: Series[], s: MetricSeries) {
68
- if (s.labelset !== undefined) {
69
- const metric = s.labelset.labels.sort((a, b) => a.name.localeCompare(b.name));
70
- agg.push({
71
- metric,
72
- isSelected: s.isSelected ?? false,
73
- values: s.samples.reduce<number[][]>(function (agg: number[][], d: MetricsSample) {
74
- if (d.timestamp !== undefined && d.value !== undefined) {
75
- agg.push([d.timestamp, d.value]);
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);
76
90
  }
77
- return agg;
78
- }, []),
79
- labelset: metric.map(m => `${m.name}=${m.value}`).join(','),
80
- });
81
- }
82
- return agg;
83
- }, []);
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
+ };
84
150
 
85
- // Sort values by timestamp for each series
86
- return series.map(series => ({
87
- ...series,
88
- values: series.values.sort((a, b) => a[0] - b[0]),
89
- }));
90
- }
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
+ };
91
172
 
92
- const getYAxisUnit = (name: string): string => {
173
+ const _getYAxisUnit = (name: string): string => {
93
174
  switch (name) {
94
175
  case 'gpu_power_watt':
95
176
  return 'watts';
@@ -104,427 +185,209 @@ const getYAxisUnit = (name: string): string => {
104
185
 
105
186
  const RawUtilizationMetrics = ({
106
187
  data,
107
- addLabelMatcher,
188
+ originalData,
108
189
  setTimeRange,
109
190
  width,
110
191
  height,
111
192
  margin,
112
- name,
113
193
  humanReadableName,
114
194
  from,
115
195
  to,
116
- onSelectedSeriesChange,
196
+ yAxisUnit,
197
+ contextMenuItems,
198
+ onSeriesClick,
117
199
  }: RawUtilizationMetricsProps): JSX.Element => {
118
200
  const {timezone} = useParcaContext();
119
- const graph = useRef(null);
120
- const [dragging, setDragging] = useState(false);
121
- const [hovering, setHovering] = useState(false);
122
- const [relPos, setRelPos] = useState(-1);
123
- const [pos, setPos] = useState([0, 0]);
124
- const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean>(false);
125
- const idForContextMenu = useId();
126
- const [_, setSelectedTimeframe] = useURLState('gpu_selected_timeframe');
127
-
128
- const lineStroke = '1px';
129
- const lineStrokeHover = '2px';
130
- const lineStrokeSelected = '3px';
131
-
132
- const graphWidth = useMemo(() => width - margin * 1.5 - margin / 2, [width, margin]);
133
- const graphTransform = useMemo(() => {
134
- // Adds 10px padding which aligns the graph on the grid
135
- return `translate(10, 0) scale(${(graphWidth - 10) / graphWidth}, 1)`;
136
- }, [graphWidth]);
137
-
138
- const paddedFrom = from;
139
- const paddedTo = to;
140
-
141
- const series = useMemo(() => transformToSeries(data), [data]);
142
-
143
- const extentsY = series.map(function (s) {
144
- return d3.extent(s.values, function (d) {
145
- return d[1];
146
- });
147
- });
148
-
149
- const minY = d3.min(extentsY, function (d) {
150
- return d[0];
151
- });
152
-
153
- const maxY = d3.max(extentsY, function (d) {
154
- return d[1];
155
- });
156
-
157
- // Setup scales with padded time range
158
- const xScale = d3.scaleUtc().domain([paddedFrom, paddedTo]).range([0, graphWidth]);
159
-
160
- const yScale = d3
161
- .scaleLinear()
162
- // tslint:disable-next-line
163
- .domain([minY, maxY] as Iterable<d3.NumberValue>)
164
- .range([height - margin, 0])
165
- .nice();
166
-
167
- const throttledSetPos = throttle(setPos, 20);
168
-
169
- const onMouseMove = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
170
- if (isContextMenuOpen) {
171
- return;
172
- }
173
-
174
- // X/Y coordinate array relative to svg
175
- const rel = pointer(e);
176
-
177
- const xCoordinate = rel[0];
178
- const xCoordinateWithoutMargin = xCoordinate - margin;
179
- const yCoordinate = rel[1];
180
- const yCoordinateWithoutMargin = yCoordinate - margin;
181
-
182
- throttledSetPos([xCoordinateWithoutMargin, yCoordinateWithoutMargin]);
183
- };
184
-
185
- const trackVisibility = (isVisible: boolean): void => {
186
- setIsContextMenuOpen(isVisible);
187
- };
188
-
189
- const MENU_ID = `utilizationmetrics-context-menu-${idForContextMenu}`;
190
-
191
- const {show} = useContextMenu({
192
- id: MENU_ID,
193
- });
194
-
195
- const displayMenu = useCallback(
196
- (e: React.MouseEvent): void => {
197
- show({
198
- event: e,
199
- });
200
- },
201
- [show]
202
- );
203
-
204
- const l = d3.line(
205
- d => xScale(d[0]),
206
- d => yScale(d[1])
207
- );
208
-
209
- const highlighted = useMemo(() => {
210
- if (series.length === 0) {
211
- return null;
212
- }
213
-
214
- // Return the closest point as the highlighted point
215
- const closestPointPerSeries = series.map(function (s) {
216
- const distances = s.values.map(d => {
217
- const x = xScale(d[0]) + margin / 2;
218
- const y = yScale(d[1]) - margin / 3;
219
-
220
- return Math.sqrt(Math.pow(pos[0] - x, 2) + Math.pow(pos[1] - y, 2));
221
- });
222
-
223
- const pointIndex = d3.minIndex(distances);
224
- const minDistance = distances[pointIndex];
225
-
226
- return {
227
- pointIndex,
228
- distance: minDistance,
229
- };
230
- });
231
-
232
- const closestSeriesIndex = d3.minIndex(closestPointPerSeries, s => s.distance);
233
- const pointIndex = closestPointPerSeries[closestSeriesIndex].pointIndex;
234
- const point = series[closestSeriesIndex].values[pointIndex];
235
- return {
236
- seriesIndex: closestSeriesIndex,
237
- labels: series[closestSeriesIndex].metric,
238
- timestamp: point[0],
239
- valuePerSecond: point[1],
240
- value: point[2],
241
- duration: point[3],
242
- x: xScale(point[0]),
243
- y: yScale(point[1]),
244
- };
245
- }, [pos, series, xScale, yScale, margin]);
246
-
247
- const onMouseDown = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
248
- // only left mouse button
249
- if (e.button !== 0) {
250
- return;
251
- }
252
-
253
- // X/Y coordinate array relative to svg
254
- const rel = pointer(e);
255
-
256
- const xCoordinate = rel[0];
257
- const xCoordinateWithoutMargin = xCoordinate - margin;
258
- if (xCoordinateWithoutMargin >= 0) {
259
- setRelPos(xCoordinateWithoutMargin);
260
- setDragging(true);
261
- }
262
-
263
- e.stopPropagation();
264
- e.preventDefault();
265
- };
266
-
267
- const onMouseUp = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
268
- setDragging(false);
269
-
270
- if (relPos === -1) {
271
- // MouseDown happened outside of this element.
272
- return;
273
- }
274
-
275
- // This is a normal click. We tolerate tiny movements to still be a
276
- // click as they can occur when clicking based on user feedback.
277
- if (Math.abs(relPos - pos[0]) <= 1) {
278
- setRelPos(-1);
279
- return;
280
- }
281
-
282
- let startPos = relPos;
283
- let endPos = pos[0];
284
-
285
- if (startPos > endPos) {
286
- startPos = pos[0];
287
- endPos = relPos;
288
- }
289
-
290
- const startCorrection = 10;
291
- const endCorrection = 30;
292
-
293
- const firstTime = xScale.invert(startPos - startCorrection).valueOf();
294
- const secondTime = xScale.invert(endPos - endCorrection).valueOf();
295
-
296
- setTimeRange(DateTimeRange.fromAbsoluteDates(firstTime, secondTime));
297
-
298
- setRelPos(-1);
299
-
300
- e.stopPropagation();
301
- e.preventDefault();
302
- };
303
201
 
304
202
  return (
305
- <>
306
- <MetricsContextMenu
307
- onAddLabelMatcher={addLabelMatcher}
308
- menuId={MENU_ID}
309
- highlighted={highlighted}
310
- trackVisibility={trackVisibility}
311
- utilizationMetrics={true}
312
- />
313
-
314
- {highlighted != null && hovering && !dragging && pos[0] !== 0 && pos[1] !== 0 && (
315
- <div
316
- onMouseMove={onMouseMove}
317
- onMouseEnter={() => setHovering(true)}
318
- onMouseLeave={() => setHovering(false)}
319
- >
320
- {!isContextMenuOpen && (
321
- <MetricsTooltip
322
- x={pos[0] + margin}
323
- y={pos[1] + margin}
324
- highlighted={highlighted}
325
- contextElement={graph.current}
326
- sampleType={name}
327
- sampleUnit={getYAxisUnit(name)}
328
- delta={false}
329
- utilizationMetrics={true}
330
- />
331
- )}
332
- </div>
333
- )}
334
- <div
335
- ref={graph}
336
- onMouseEnter={function () {
337
- setHovering(true);
338
- }}
339
- onMouseLeave={() => setHovering(false)}
340
- onContextMenu={displayMenu}
341
- >
342
- <svg
343
- width={`${width}px`}
344
- height={`${height + margin}px`}
345
- onMouseDown={onMouseDown}
346
- onMouseUp={onMouseUp}
347
- onMouseMove={onMouseMove}
348
- >
349
- <g transform={`translate(${margin}, 0)`}>
350
- {dragging && (
351
- <g className="zoom-time-rect">
352
- <rect
353
- className="bar"
354
- x={pos[0] - relPos < 0 ? pos[0] : relPos}
355
- y={0}
356
- height={height}
357
- width={Math.abs(pos[0] - relPos)}
358
- fill={'rgba(0, 0, 0, 0.125)'}
359
- />
360
- </g>
361
- )}
362
- </g>
363
- <g transform={`translate(${margin * 1.5}, ${margin / 1.5})`}>
364
- <g className="y axis" textAnchor="end" fontSize="10" fill="none">
365
- {yScale.ticks(3).map((d, i, allTicks) => {
366
- let decimals = 2;
367
- const intervalBetweenTicks = allTicks[1] - allTicks[0];
368
-
369
- if (intervalBetweenTicks < 1) {
370
- const precision = getPrecision(intervalBetweenTicks);
371
- decimals = precision;
372
- }
373
-
374
- return (
375
- <Fragment key={`${i.toString()}-${d.toString()}`}>
376
- <g
377
- key={`tick-${i}`}
378
- className="tick"
379
- /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
380
- transform={`translate(0, ${yScale(d)})`}
381
- >
382
- <line className="stroke-gray-300 dark:stroke-gray-500" x2={-6} />
383
- <text fill="currentColor" x={-9} dy={'0.32em'}>
384
- {valueFormatter(d, getYAxisUnit(name), decimals)}
385
- </text>
386
- </g>
387
- <g key={`grid-${i}`}>
388
- <line
389
- className="stroke-gray-300 dark:stroke-gray-500"
390
- x1={xScale(from)}
391
- x2={xScale(to)}
392
- y1={yScale(d)}
393
- y2={yScale(d)}
394
- />
395
- </g>
396
- </Fragment>
397
- );
398
- })}
399
- <line
400
- className="stroke-gray-300 dark:stroke-gray-500"
401
- x1={0}
402
- x2={0}
403
- y1={0}
404
- y2={height - margin}
405
- />
406
- <line
407
- className="stroke-gray-300 dark:stroke-gray-500"
408
- x1={xScale(to)}
409
- x2={xScale(to)}
410
- y1={0}
411
- y2={height - margin}
412
- />
413
- <g transform={`translate(${-margin}, ${(height - margin) / 2}) rotate(270)`}>
414
- <text
415
- fill="currentColor"
416
- dy="-0.7em"
417
- className="text-sm capitalize"
418
- textAnchor="middle"
419
- >
420
- {humanReadableName}
421
- </text>
422
- </g>
423
- </g>
424
- <g
425
- className="x axis"
426
- fill="none"
427
- fontSize="10"
428
- textAnchor="middle"
429
- transform={`translate(0,${height - margin})`}
430
- >
431
- {xScale.ticks(5).map((d, i) => (
432
- <Fragment key={`${i.toString()}-${d.toString()}`}>
433
- <g
434
- key={`tick-${i}`}
435
- className="tick"
436
- /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
437
- transform={`translate(${xScale(d)}, 0)`}
438
- >
439
- <line y2={6} className="stroke-gray-300 dark:stroke-gray-500" />
440
- <text fill="currentColor" dy=".71em" y={9}>
441
- {formatDate(d, formatForTimespan(from, to), timezone)}
442
- </text>
443
- </g>
444
- <g key={`grid-${i}`}>
445
- <line
446
- className="stroke-gray-300 dark:stroke-gray-500"
447
- x1={xScale(d)}
448
- x2={xScale(d)}
449
- y1={0}
450
- y2={-height + margin}
451
- />
452
- </g>
453
- </Fragment>
454
- ))}
455
- <line
456
- className="stroke-gray-300 dark:stroke-gray-500"
457
- x1={0}
458
- x2={graphWidth}
459
- y1={0}
460
- y2={0}
461
- />
462
- <g transform={`translate(${(width - 2.5 * margin) / 2}, ${margin / 2})`}>
463
- <text fill="currentColor" dy=".71em" y={5} className="text-sm">
464
- Time
465
- </text>
466
- </g>
467
- </g>
468
- <g className="lines fill-transparent" transform={graphTransform}>
469
- {series.map((s, i) => {
470
- const isLimit =
471
- s.metric.findIndex(m => m.name === '__type__' && m.value === 'limit') > -1;
472
- const strokeDasharray = isLimit ? '8 4' : '';
473
-
474
- return (
475
- <g key={i} className="line cursor-pointer">
476
- <MetricsSeries
477
- data={s}
478
- line={l}
479
- color={getSeriesColor(s.metric)}
480
- strokeWidth={
481
- s.isSelected === true
482
- ? lineStrokeSelected
483
- : hovering && highlighted != null && i === highlighted.seriesIndex
484
- ? lineStrokeHover
485
- : lineStroke
486
- }
487
- strokeDasharray={strokeDasharray}
488
- xScale={xScale}
489
- yScale={yScale}
490
- onClick={() => {
491
- if (highlighted != null && onSelectedSeriesChange != null) {
492
- onSelectedSeriesChange(
493
- highlighted.labels.map(l => ({
494
- key: l.name,
495
- value: l.value,
496
- }))
497
- );
498
- // reset the selected_timeframe
499
- setSelectedTimeframe(undefined);
203
+ <MetricsGraph
204
+ data={data}
205
+ from={from}
206
+ to={to}
207
+ setTimeRange={setTimeRange}
208
+ onSampleClick={closestPoint => {
209
+ if (onSeriesClick != null) {
210
+ onSeriesClick(closestPoint.seriesIndex);
211
+ }
212
+ }}
213
+ yAxisLabel={humanReadableName}
214
+ yAxisUnit={yAxisUnit}
215
+ width={width}
216
+ height={height}
217
+ margin={margin}
218
+ contextMenuItems={contextMenuItems}
219
+ renderTooltipContent={(seriesIndex: number, pointIndex: number) => {
220
+ if (originalData?.[seriesIndex]?.samples?.[pointIndex] != null) {
221
+ const originalSeriesData = originalData[seriesIndex];
222
+ const originalPoint = originalData[seriesIndex].samples[pointIndex];
223
+
224
+ const labels = originalSeriesData.labelset?.labels ?? [];
225
+ const nameLabel = labels.find(e => e.name === '__name__');
226
+ const highlightedNameLabel = nameLabel ?? {name: '', value: ''};
227
+
228
+ // Calculate attributes maps for utilization metrics
229
+ const attributesMap = labels
230
+ .filter(
231
+ label =>
232
+ label.name.startsWith('attributes.') &&
233
+ !label.name.startsWith('attributes_resource.')
234
+ )
235
+ .reduce<Record<string, string>>((acc, label) => {
236
+ const key = label.name.replace('attributes.', '');
237
+ acc[key] = label.value;
238
+ return acc;
239
+ }, {});
240
+
241
+ const attributesResourceMap = labels
242
+ .filter(label => label.name.startsWith('attributes_resource.'))
243
+ .reduce<Record<string, string>>((acc, label) => {
244
+ const key = label.name.replace('attributes_resource.', '');
245
+ acc[key] = label.value;
246
+ return acc;
247
+ }, {});
248
+
249
+ return (
250
+ <div className="flex flex-row">
251
+ <div className="ml-2 mr-6">
252
+ <span className="font-semibold">{highlightedNameLabel.value}</span>
253
+ <span className="my-2 block text-gray-700 dark:text-gray-300">
254
+ <table className="table-auto">
255
+ <tbody>
256
+ <tr>
257
+ <td className="w-1/4">Value</td>
258
+ <td className="w-3/4">
259
+ {valueFormatter(originalPoint.value, yAxisUnit, 2)}
260
+ </td>
261
+ </tr>
262
+ <tr>
263
+ <td className="w-1/4">At</td>
264
+ <td className="w-3/4">
265
+ {formatDate(
266
+ new Date(originalPoint.timestamp),
267
+ timePattern(timezone as string),
268
+ timezone
269
+ )}
270
+ </td>
271
+ </tr>
272
+ </tbody>
273
+ </table>
274
+ </span>
275
+ <span className="my-2 block text-gray-500">
276
+ {Object.keys(attributesResourceMap).length > 0 ? (
277
+ <span className="text-sm font-bold text-gray-700 dark:text-white">
278
+ Resource Attributes
279
+ </span>
280
+ ) : null}
281
+ <span className="my-2 block text-gray-500">
282
+ {Object.keys(attributesResourceMap).map(name => (
283
+ <div
284
+ key={
285
+ 'resourceattribute-' +
286
+ seriesIndex.toString() +
287
+ '-' +
288
+ pointIndex.toString() +
289
+ '-' +
290
+ name
500
291
  }
501
- }}
502
- />
503
- </g>
504
- );
505
- })}
506
- </g>
507
- </g>
508
- </svg>
509
- </div>
510
- </>
292
+ 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"
293
+ >
294
+ <TextWithTooltip
295
+ text={`${transformUtilizationLabels(name)}="${
296
+ attributesResourceMap[name] ?? ''
297
+ }"`}
298
+ maxTextLength={48}
299
+ id={`tooltip-${name}-${attributesResourceMap[name] ?? ''}`}
300
+ />
301
+ </div>
302
+ ))}
303
+ </span>
304
+ {Object.keys(attributesMap).length > 0 ? (
305
+ <span className="text-sm font-bold text-gray-700 dark:text-white">
306
+ Attributes
307
+ </span>
308
+ ) : null}
309
+ <span className="my-2 block text-gray-500">
310
+ {Object.keys(attributesMap).map(name => (
311
+ <div
312
+ key={
313
+ 'attribute-' +
314
+ seriesIndex.toString() +
315
+ '-' +
316
+ pointIndex.toString() +
317
+ '-' +
318
+ name
319
+ }
320
+ 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"
321
+ >
322
+ <TextWithTooltip
323
+ text={`${transformUtilizationLabels(name)}="${
324
+ attributesMap[name] ?? ''
325
+ }"`}
326
+ maxTextLength={48}
327
+ id={`tooltip-${name}-${attributesMap[name] ?? ''}`}
328
+ />
329
+ </div>
330
+ ))}
331
+ </span>
332
+ {labels
333
+ .filter(
334
+ label => label.name !== '__name__' && !label.name.startsWith('attributes')
335
+ )
336
+ .map(label => (
337
+ <div
338
+ key={
339
+ 'attribute-' +
340
+ seriesIndex.toString() +
341
+ '-' +
342
+ pointIndex.toString() +
343
+ '-label-' +
344
+ label.name
345
+ }
346
+ 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"
347
+ >
348
+ <TextWithTooltip
349
+ text={`${transformUtilizationLabels(label.name)}="${label.value}"`}
350
+ maxTextLength={37}
351
+ id={`tooltip-${label.name}`}
352
+ />
353
+ </div>
354
+ ))}
355
+ </span>
356
+ <div className="flex w-full items-center gap-1 text-xs text-gray-500">
357
+ <Icon icon="iconoir:mouse-button-right" />
358
+ <div>Right click to add labels to query.</div>
359
+ </div>
360
+ </div>
361
+ </div>
362
+ );
363
+ }
364
+ return null;
365
+ }}
366
+ />
511
367
  );
512
368
  };
513
369
 
514
370
  const UtilizationMetrics = ({
515
371
  data,
516
- addLabelMatcher,
517
372
  setTimeRange,
518
373
  utilizationMetricsLoading,
519
- name,
520
374
  humanReadableName,
521
375
  from,
522
376
  to,
523
- onSelectedSeriesChange,
377
+ yAxisUnit,
378
+ addLabelMatcher,
379
+ onSeriesClick,
380
+ onSelectedSeriesChange: _onSelectedSeriesChange,
524
381
  }: Props): JSX.Element => {
525
382
  const {isDarkMode} = useParcaContext();
526
383
  const {width, height, margin, heightStyle} = useMetricsGraphDimensions(false, true);
527
384
 
385
+ const transformedData = useMemo(() => transformMetricSeriesToSeries(data), [data]);
386
+
387
+ const contextMenuItems = useMemo(() => {
388
+ return addLabelMatcher != null ? createUtilizationContextMenuItems(addLabelMatcher, data) : [];
389
+ }, [addLabelMatcher, data]);
390
+
528
391
  return (
529
392
  <AnimatePresence>
530
393
  <motion.div
@@ -538,17 +401,18 @@ const UtilizationMetrics = ({
538
401
  <MetricsGraphSkeleton heightStyle={heightStyle} isDarkMode={isDarkMode} isMini={true} />
539
402
  ) : (
540
403
  <RawUtilizationMetrics
541
- data={data}
542
- addLabelMatcher={addLabelMatcher}
404
+ data={transformedData}
405
+ originalData={data}
543
406
  setTimeRange={setTimeRange}
544
407
  width={width}
545
408
  height={height}
546
409
  margin={margin}
547
- name={name}
548
410
  humanReadableName={humanReadableName}
549
411
  from={from}
550
412
  to={to}
551
- onSelectedSeriesChange={onSelectedSeriesChange}
413
+ yAxisUnit={yAxisUnit}
414
+ contextMenuItems={contextMenuItems}
415
+ onSeriesClick={onSeriesClick}
552
416
  />
553
417
  )}
554
418
  </motion.div>