@parca/profile 0.19.44 → 0.19.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/GraphTooltipArrow/Content.d.ts.map +1 -1
  3. package/dist/GraphTooltipArrow/Content.js +1 -1
  4. package/dist/MetricsGraph/MetricsContextMenu/index.d.ts +20 -11
  5. package/dist/MetricsGraph/MetricsContextMenu/index.d.ts.map +1 -1
  6. package/dist/MetricsGraph/MetricsContextMenu/index.js +16 -20
  7. package/dist/MetricsGraph/MetricsTooltip/index.d.ts +2 -8
  8. package/dist/MetricsGraph/MetricsTooltip/index.d.ts.map +1 -1
  9. package/dist/MetricsGraph/MetricsTooltip/index.js +46 -55
  10. package/dist/MetricsGraph/UtilizationMetrics/Throughput.d.ts +2 -5
  11. package/dist/MetricsGraph/UtilizationMetrics/Throughput.d.ts.map +1 -1
  12. package/dist/MetricsGraph/UtilizationMetrics/Throughput.js +126 -205
  13. package/dist/MetricsGraph/UtilizationMetrics/index.d.ts +9 -17
  14. package/dist/MetricsGraph/UtilizationMetrics/index.d.ts.map +1 -1
  15. package/dist/MetricsGraph/UtilizationMetrics/index.js +149 -208
  16. package/dist/MetricsGraph/index.d.ts +19 -26
  17. package/dist/MetricsGraph/index.d.ts.map +1 -1
  18. package/dist/MetricsGraph/index.js +50 -115
  19. package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
  20. package/dist/ProfileFlameGraph/index.js +3 -1
  21. package/dist/ProfileMetricsGraph/index.d.ts +1 -1
  22. package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
  23. package/dist/ProfileMetricsGraph/index.js +232 -23
  24. package/dist/ProfileSelector/MetricsGraphSection.d.ts +1 -4
  25. package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
  26. package/dist/ProfileSelector/MetricsGraphSection.js +8 -4
  27. package/dist/ProfileSelector/index.d.ts +3 -6
  28. package/dist/ProfileSelector/index.d.ts.map +1 -1
  29. package/dist/ProfileSelector/index.js +2 -2
  30. package/dist/ProfileSource.d.ts +9 -6
  31. package/dist/ProfileSource.d.ts.map +1 -1
  32. package/dist/ProfileSource.js +23 -8
  33. package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts.map +1 -1
  34. package/dist/ProfileView/components/GroupByLabelsDropdown/index.js +5 -1
  35. package/dist/ProfileView/components/ProfileFilters/index.d.ts.map +1 -1
  36. package/dist/ProfileView/components/ProfileFilters/index.js +6 -5
  37. package/dist/styles.css +1 -1
  38. package/dist/useQuery.js +1 -1
  39. package/package.json +7 -7
  40. package/src/GraphTooltipArrow/Content.tsx +2 -4
  41. package/src/MetricsGraph/MetricsContextMenu/index.tsx +78 -66
  42. package/src/MetricsGraph/MetricsTooltip/index.tsx +53 -210
  43. package/src/MetricsGraph/UtilizationMetrics/Throughput.tsx +242 -434
  44. package/src/MetricsGraph/UtilizationMetrics/index.tsx +312 -448
  45. package/src/MetricsGraph/index.tsx +99 -185
  46. package/src/ProfileFlameGraph/index.tsx +3 -1
  47. package/src/ProfileMetricsGraph/index.tsx +430 -37
  48. package/src/ProfileSelector/MetricsGraphSection.tsx +12 -8
  49. package/src/ProfileSelector/index.tsx +5 -5
  50. package/src/ProfileSource.tsx +34 -17
  51. package/src/ProfileView/components/GroupByLabelsDropdown/index.tsx +15 -3
  52. package/src/ProfileView/components/ProfileFilters/index.tsx +23 -3
  53. package/src/useQuery.tsx +1 -1
@@ -17,15 +17,15 @@ import { pointer } from 'd3-selection';
17
17
  import throttle from 'lodash.throttle';
18
18
  import { useContextMenu } from 'react-contexify';
19
19
  import { DateTimeRange, useParcaContext } from '@parca/components';
20
- import { formatDate, formatForTimespan, getPrecision, sanitizeHighlightedValues, valueFormatter, } from '@parca/utilities';
20
+ import { formatDate, formatForTimespan, getPrecision, valueFormatter } from '@parca/utilities';
21
21
  import MetricsCircle from '../MetricsCircle';
22
22
  import MetricsSeries from '../MetricsSeries';
23
23
  import MetricsContextMenu from './MetricsContextMenu';
24
24
  import MetricsInfoPanel from './MetricsInfoPanel';
25
25
  import MetricsTooltip from './MetricsTooltip';
26
- const MetricsGraph = ({ data, from, to, profile, onSampleClick, addLabelMatcher, setTimeRange, sampleType, sampleUnit, width = 0, height = 0, margin = 0, sumBy, }) => {
26
+ const MetricsGraph = ({ data, from, to, onSampleClick, setTimeRange, yAxisLabel, yAxisUnit, width = 0, height = 0, margin = 0, selectedPoint, contextMenuItems, renderTooltipContent, }) => {
27
27
  const [isInfoPanelOpen, setIsInfoPanelOpen] = useState(false);
28
- return (_jsxs("div", { className: "relative", onClick: () => isInfoPanelOpen && setIsInfoPanelOpen(false), children: [_jsx("div", { className: "absolute right-0 top-0", children: _jsx(MetricsInfoPanel, { isInfoPanelOpen: isInfoPanelOpen, onInfoIconClick: () => setIsInfoPanelOpen(true) }) }), _jsx(RawMetricsGraph, { data: data, from: from, to: to, profile: profile, onSampleClick: onSampleClick, addLabelMatcher: addLabelMatcher, setTimeRange: setTimeRange, sampleType: sampleType, sampleUnit: sampleUnit, width: width, height: height, margin: margin, sumBy: sumBy })] }));
28
+ return (_jsxs("div", { className: "relative", onClick: () => isInfoPanelOpen && setIsInfoPanelOpen(false), children: [_jsx("div", { className: "absolute right-0 top-0", children: _jsx(MetricsInfoPanel, { isInfoPanelOpen: isInfoPanelOpen, onInfoIconClick: () => setIsInfoPanelOpen(true) }) }), _jsx(RawMetricsGraph, { data: data, from: from, to: to, onSampleClick: onSampleClick, setTimeRange: setTimeRange, yAxisLabel: yAxisLabel, yAxisUnit: yAxisUnit, width: width, height: height, margin: margin, selectedPoint: selectedPoint, contextMenuItems: contextMenuItems, renderTooltipContent: renderTooltipContent })] }));
29
29
  };
30
30
  export default MetricsGraph;
31
31
  export const parseValue = (value) => {
@@ -36,7 +36,7 @@ export const parseValue = (value) => {
36
36
  };
37
37
  const lineStroke = '1px';
38
38
  const lineStrokeHover = '2px';
39
- export const RawMetricsGraph = ({ data, from, to, profile, onSampleClick, addLabelMatcher, setTimeRange, sampleType, sampleUnit, width, height = 50, margin = 0, sumBy, }) => {
39
+ export const RawMetricsGraph = ({ data, from, to, onSampleClick, setTimeRange, yAxisLabel, yAxisUnit, width, height = 50, margin = 0, selectedPoint, contextMenuItems, renderTooltipContent, }) => {
40
40
  const { timezone } = useParcaContext();
41
41
  const graph = useRef(null);
42
42
  const [dragging, setDragging] = useState(false);
@@ -46,8 +46,6 @@ export const RawMetricsGraph = ({ data, from, to, profile, onSampleClick, addLab
46
46
  const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
47
47
  const metricPointRef = useRef(null);
48
48
  const idForContextMenu = useId();
49
- // the time of the selected point is the start of the merge window
50
- const time = parseFloat(profile?.HistoryParams().merge_from);
51
49
  if (width === undefined || width == null) {
52
50
  width = 0;
53
51
  }
@@ -56,28 +54,10 @@ export const RawMetricsGraph = ({ data, from, to, profile, onSampleClick, addLab
56
54
  // Adds 6px padding which aligns the graph on the grid
57
55
  return `translate(6, 0) scale(${(graphWidth - 6) / graphWidth}, 1)`;
58
56
  }, [graphWidth]);
59
- const series = data.reduce(function (agg, s) {
60
- if (s.labelset !== undefined) {
61
- const metric = s.labelset.labels.sort((a, b) => a.name.localeCompare(b.name));
62
- agg.push({
63
- metric,
64
- values: s.samples.reduce(function (agg, d) {
65
- if (d.timestamp !== undefined && d.valuePerSecond !== undefined) {
66
- const t = (Number(d.timestamp.seconds) * 1e9 + d.timestamp.nanos) / 1e6; // https://github.com/microsoft/TypeScript/issues/5710#issuecomment-157886246
67
- agg.push([t, d.valuePerSecond, Number(d.value), Number(d.duration)]);
68
- }
69
- return agg;
70
- }, []),
71
- labelset: metric.map(m => `${m.name}=${m.value}`).join(','),
72
- });
73
- }
74
- return agg;
75
- }, []);
76
- // Sort series by id to make sure the colors are consistent
77
- series.sort((a, b) => a.labelset.localeCompare(b.labelset));
57
+ const series = data;
78
58
  const extentsY = series.map(function (s) {
79
59
  return d3.extent(s.values, function (d) {
80
- return d[1];
60
+ return d[1]; // d[1] is the value
81
61
  });
82
62
  });
83
63
  const minY = d3.min(extentsY, function (d) {
@@ -94,14 +74,22 @@ export const RawMetricsGraph = ({ data, from, to, profile, onSampleClick, addLab
94
74
  .domain([minY, maxY])
95
75
  .range([height - margin, 0])
96
76
  .nice();
97
- const color = d3.scaleOrdinal(d3.schemeCategory10);
77
+ // Create deterministic color mapping based on series IDs
78
+ const color = useMemo(() => {
79
+ const scale = d3.scaleOrdinal(d3.schemeCategory10);
80
+ // Pre-populate the scale with sorted series IDs to ensure consistent colors
81
+ const sortedIds = [...new Set(series.map(s => s.id))].sort();
82
+ sortedIds.forEach(id => scale(id));
83
+ return scale;
84
+ }, [series]);
98
85
  const l = d3.line(d => xScale(d[0]), d => yScale(d[1]));
99
- const highlighted = useMemo(() => {
86
+ const closestPoint = useMemo(() => {
100
87
  // Return the closest point as the highlighted point
101
88
  const closestPointPerSeries = series.map(function (s) {
102
89
  const distances = s.values.map(d => {
103
- const x = xScale(d[0]) + margin / 2;
104
- const y = yScale(d[1]) - margin / 3;
90
+ const x = xScale(d[0]) + margin / 2; // d[0] is timestamp_ms
91
+ const y = yScale(d[1]) - margin / 3; // d[1] is value
92
+ // Cartesian distance from the mouse position to the point
105
93
  return Math.sqrt(Math.pow(pos[0] - x, 2) + Math.pow(pos[1] - y, 2));
106
94
  });
107
95
  const pointIndex = d3.minIndex(distances);
@@ -113,18 +101,35 @@ export const RawMetricsGraph = ({ data, from, to, profile, onSampleClick, addLab
113
101
  });
114
102
  const closestSeriesIndex = d3.minIndex(closestPointPerSeries, s => s.distance);
115
103
  const pointIndex = closestPointPerSeries[closestSeriesIndex].pointIndex;
116
- const point = series[closestSeriesIndex].values[pointIndex];
117
104
  return {
118
105
  seriesIndex: closestSeriesIndex,
119
- labels: series[closestSeriesIndex].metric,
120
- timestamp: point[0],
121
- valuePerSecond: point[1],
122
- value: point[2],
123
- duration: point[3],
106
+ pointIndex,
107
+ };
108
+ }, [pos, series, xScale, yScale, margin]);
109
+ const highlighted = useMemo(() => {
110
+ if (series.length === 0 || closestPoint == null) {
111
+ return null;
112
+ }
113
+ const point = series[closestPoint.seriesIndex].values[closestPoint.pointIndex];
114
+ return {
115
+ seriesIndex: closestPoint.seriesIndex,
116
+ pointIndex: closestPoint.pointIndex,
124
117
  x: xScale(point[0]),
125
118
  y: yScale(point[1]),
126
119
  };
127
- }, [pos, series, xScale, yScale, margin]);
120
+ }, [closestPoint, series, xScale, yScale]);
121
+ const selected = useMemo(() => {
122
+ if (series.length === 0 || selectedPoint == null) {
123
+ return null;
124
+ }
125
+ const point = series[selectedPoint.seriesIndex].values[selectedPoint.pointIndex];
126
+ return {
127
+ seriesIndex: selectedPoint.seriesIndex,
128
+ pointIndex: selectedPoint.pointIndex,
129
+ x: xScale(point[0]),
130
+ y: yScale(point[1]),
131
+ };
132
+ }, [selectedPoint, series, xScale, yScale]);
128
133
  const onMouseDown = (e) => {
129
134
  // only left mouse button
130
135
  if (e.button !== 0) {
@@ -141,10 +146,9 @@ export const RawMetricsGraph = ({ data, from, to, profile, onSampleClick, addLab
141
146
  e.stopPropagation();
142
147
  e.preventDefault();
143
148
  };
144
- const openClosestProfile = () => {
145
- if (highlighted != null) {
146
- onSampleClick(Math.round(highlighted.timestamp), highlighted.value, sanitizeHighlightedValues(highlighted.labels), // When a user clicks on any sample in the graph, replace single `\` in the `labelValues` string with doubles `\\` if available.
147
- highlighted.duration);
149
+ const handleClosestPointClick = () => {
150
+ if (closestPoint != null) {
151
+ onSampleClick(closestPoint);
148
152
  }
149
153
  };
150
154
  const onMouseUp = (e) => {
@@ -156,7 +160,7 @@ export const RawMetricsGraph = ({ data, from, to, profile, onSampleClick, addLab
156
160
  // This is a normal click. We tolerate tiny movements to still be a
157
161
  // click as they can occur when clicking based on user feedback.
158
162
  if (Math.abs(relPos - pos[0]) <= 1) {
159
- openClosestProfile();
163
+ handleClosestPointClick();
160
164
  setRelPos(-1);
161
165
  return;
162
166
  }
@@ -188,58 +192,6 @@ export const RawMetricsGraph = ({ data, from, to, profile, onSampleClick, addLab
188
192
  const yCoordinateWithoutMargin = yCoordinate - margin;
189
193
  throttledSetPos([xCoordinateWithoutMargin, yCoordinateWithoutMargin]);
190
194
  };
191
- const findSelectedProfile = () => {
192
- if (profile == null) {
193
- return null;
194
- }
195
- let s = null;
196
- let seriesIndex = -1;
197
- // 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.
198
- // if it is, then we should prioritize using the sumby label name and value to find the selected profile.
199
- const useSumBy = sumBy !== undefined &&
200
- sumBy.length > 0 &&
201
- profile.query.matchers.length > 0 &&
202
- profile.query.matchers.some(e => sumBy.includes(e.key));
203
- // get only the sumby keys and values from the profile query matchers
204
- const sumByMatchers = sumBy !== undefined ? profile.query.matchers.filter(e => sumBy.includes(e.key)) : [];
205
- const keysToMatch = useSumBy ? sumByMatchers : profile.query.matchers;
206
- outer: for (let i = 0; i < series.length; i++) {
207
- const keys = keysToMatch.map(e => e.key);
208
- for (let j = 0; j < keys.length; j++) {
209
- const matcherKey = keys[j];
210
- const label = series[i].metric.find(e => e.name === matcherKey);
211
- if (label === undefined) {
212
- continue outer; // label doesn't exist to begin with
213
- }
214
- if (keysToMatch[j].value !== label.value) {
215
- continue outer; // label values don't match
216
- }
217
- }
218
- seriesIndex = i;
219
- s = series[i];
220
- }
221
- if (s == null) {
222
- return null;
223
- }
224
- // Find the sample that matches the timestamp
225
- const sample = s.values.find(v => {
226
- return Math.round(v[0]) === time;
227
- });
228
- if (sample === undefined) {
229
- return null;
230
- }
231
- return {
232
- labels: [],
233
- seriesIndex,
234
- timestamp: sample[0],
235
- valuePerSecond: sample[1],
236
- value: sample[2],
237
- duration: sample[3],
238
- x: xScale(sample[0]),
239
- y: yScale(sample[1]),
240
- };
241
- };
242
- const selected = findSelectedProfile();
243
195
  const MENU_ID = `metrics-context-menu-${idForContextMenu}`;
244
196
  const { show } = useContextMenu({
245
197
  id: MENU_ID,
@@ -252,24 +204,7 @@ export const RawMetricsGraph = ({ data, from, to, profile, onSampleClick, addLab
252
204
  const trackVisibility = (isVisible) => {
253
205
  setIsContextMenuOpen(isVisible);
254
206
  };
255
- const isDeltaType = profile !== null ? profile?.query.profType.delta : false;
256
- let yAxisLabel = sampleUnit;
257
- let yAxisUnit = sampleUnit;
258
- if (isDeltaType) {
259
- if (sampleUnit === 'nanoseconds') {
260
- if (sampleType === 'cpu') {
261
- yAxisLabel = 'CPU Cores';
262
- yAxisUnit = '';
263
- }
264
- if (sampleType === 'cuda') {
265
- yAxisLabel = 'GPU Time';
266
- }
267
- }
268
- if (sampleUnit === 'bytes') {
269
- yAxisLabel = 'Bytes per Second';
270
- }
271
- }
272
- return (_jsxs(_Fragment, { children: [_jsx(MetricsContextMenu, { onAddLabelMatcher: addLabelMatcher, menuId: MENU_ID, highlighted: highlighted, trackVisibility: trackVisibility }), highlighted != null && hovering && !dragging && pos[0] !== 0 && pos[1] !== 0 && (_jsx("div", { onMouseMove: onMouseMove, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false), children: !isContextMenuOpen && (_jsx(MetricsTooltip, { x: pos[0] + margin, y: pos[1] + margin, highlighted: highlighted, contextElement: graph.current, sampleType: sampleType, sampleUnit: sampleUnit, delta: isDeltaType })) })), _jsx("div", { ref: graph, onMouseEnter: function () {
207
+ return (_jsxs(_Fragment, { children: [contextMenuItems != null && (_jsx(MetricsContextMenu, { menuId: MENU_ID, closestPoint: closestPoint, series: series, trackVisibility: trackVisibility, menuItems: contextMenuItems })), highlighted != null && hovering && !dragging && pos[0] !== 0 && pos[1] !== 0 && (_jsx("div", { onMouseMove: onMouseMove, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false), children: !isContextMenuOpen && (_jsx(MetricsTooltip, { x: pos[0] + margin, y: pos[1] + margin, contextElement: graph.current, content: renderTooltipContent?.(highlighted.seriesIndex, highlighted.pointIndex) })) })), _jsx("div", { ref: graph, onMouseEnter: function () {
273
208
  setHovering(true);
274
209
  }, onMouseLeave: () => setHovering(false), onContextMenu: displayMenu, children: _jsxs("svg", { width: `${width}px`, height: `${height + margin}px`, onMouseDown: onMouseDown, onMouseUp: onMouseUp, onMouseMove: onMouseMove, children: [_jsx("g", { transform: `translate(${margin}, 0)`, children: dragging && (_jsx("g", { className: "zoom-time-rect", children: _jsx("rect", { className: "bar", x: pos[0] - relPos < 0 ? pos[0] : relPos, y: 0, height: height, width: Math.abs(pos[0] - relPos), fill: 'rgba(0, 0, 0, 0.125)' }) })) }), _jsxs("g", { transform: `translate(${margin * 1.5}, ${margin / 1.5})`, children: [_jsxs("g", { className: "y axis", textAnchor: "end", fontSize: "10", fill: "none", children: [yScale.ticks(5).map((d, i, allTicks) => {
275
210
  let decimals = 2;
@@ -283,9 +218,9 @@ export const RawMetricsGraph = ({ data, from, to, profile, onSampleClick, addLab
283
218
  transform: `translate(0, ${yScale(d)})`, children: [_jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x2: -6 }), _jsx("text", { fill: "currentColor", x: -9, dy: '0.32em', children: valueFormatter(d, yAxisUnit, decimals) })] }, `tick-${i}`), _jsx("g", { children: _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(from), x2: xScale(to), y1: yScale(d), y2: yScale(d) }) }, `grid-${i}`)] }, `${i.toString()}-${d.toString()}`));
284
219
  }), _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: 0, x2: 0, y1: 0, y2: height - margin }), _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(to), x2: xScale(to), y1: 0, y2: height - margin }), _jsx("g", { transform: `translate(${-margin}, ${(height - margin) / 2}) rotate(270)`, children: _jsx("text", { fill: "currentColor", dy: "-0.7em", className: "text-sm capitalize", textAnchor: "middle", children: yAxisLabel }) })] }), _jsxs("g", { className: "x axis", fill: "none", fontSize: "10", textAnchor: "middle", transform: `translate(0,${height - margin})`, children: [xScale.ticks(5).map((d, i) => (_jsxs(Fragment, { children: [_jsxs("g", { className: "tick",
285
220
  /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
286
- transform: `translate(${xScale(d)}, 0)`, children: [_jsx("line", { y2: 6, className: "stroke-gray-300 dark:stroke-gray-500" }), _jsx("text", { fill: "currentColor", dy: ".71em", y: 9, children: formatDate(d, formatForTimespan(from, to), timezone) })] }, `tick-${i}`), _jsx("g", { children: _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(d), x2: xScale(d), y1: 0, y2: -height + margin }) }, `grid-${i}`)] }, `${i.toString()}-${d.toString()}`))), _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: 0, x2: graphWidth, y1: 0, y2: 0 }), _jsx("g", { transform: `translate(${(width - 2.5 * margin) / 2}, ${margin / 2})`, children: _jsx("text", { fill: "currentColor", dy: ".71em", y: 5, className: "text-sm", children: "Time" }) })] }), _jsx("g", { className: "lines fill-transparent", transform: graphTransform, width: graphWidth - 100, children: series.map((s, i) => (_jsx("g", { className: "line", children: _jsx(MetricsSeries, { data: s, line: l, color: color(i.toString()), strokeWidth: hovering && highlighted != null && i === highlighted.seriesIndex
221
+ transform: `translate(${xScale(d)}, 0)`, children: [_jsx("line", { y2: 6, className: "stroke-gray-300 dark:stroke-gray-500" }), _jsx("text", { fill: "currentColor", dy: ".71em", y: 9, children: formatDate(d, formatForTimespan(from, to), timezone) })] }, `tick-${i}`), _jsx("g", { children: _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(d), x2: xScale(d), y1: 0, y2: -height + margin }) }, `grid-${i}`)] }, `${i.toString()}-${d.toString()}`))), _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: 0, x2: graphWidth, y1: 0, y2: 0 }), _jsx("g", { transform: `translate(${(width - 2.5 * margin) / 2}, ${margin / 2})`, children: _jsx("text", { fill: "currentColor", dy: ".71em", y: 5, className: "text-sm", children: "Time" }) })] }), _jsx("g", { className: "lines fill-transparent", transform: graphTransform, width: graphWidth - 100, children: series.map((s, i) => (_jsx("g", { className: "line", children: _jsx(MetricsSeries, { data: s, line: l, color: color(s.id), strokeWidth: hovering && highlighted != null && i === highlighted.seriesIndex
287
222
  ? lineStrokeHover
288
- : lineStroke, xScale: xScale, yScale: yScale }) }, i))) }), hovering && highlighted != null && (_jsx("g", { className: "circle-group", ref: metricPointRef, style: { fill: color(highlighted.seriesIndex.toString()) }, transform: graphTransform, children: _jsx(MetricsCircle, { cx: highlighted.x, cy: highlighted.y }) })), selected != null && (_jsx("g", { className: "circle-group", style: selected?.seriesIndex != null
289
- ? { fill: color(selected.seriesIndex.toString()) }
223
+ : lineStroke, xScale: xScale, yScale: yScale }) }, s.id))) }), hovering && highlighted != null && (_jsx("g", { className: "circle-group", ref: metricPointRef, style: { fill: color(series[highlighted.seriesIndex]?.id ?? '0') }, transform: graphTransform, children: _jsx(MetricsCircle, { cx: highlighted.x, cy: highlighted.y }) })), selected != null && (_jsx("g", { className: "circle-group", style: selected?.seriesIndex != null
224
+ ? { fill: color(series[selected.seriesIndex]?.id ?? '0') }
290
225
  : {}, transform: graphTransform, children: _jsx(MetricsCircle, { cx: selected.x, cy: selected.y, radius: 5 }) }))] })] }) })] }));
291
226
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileFlameGraph/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAwE,MAAM,OAAO,CAAC;AAM7F,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAO9C,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAI1C,OAAO,EAAC,mBAAmB,EAAE,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAMpE,OAAO,EAAC,gBAAgB,EAA0B,MAAM,yBAAyB,CAAC;AAIlF,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAEpE,UAAU,sBAAsB;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC;IACtC,kBAAkB,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,KAAK,IAAI,CAAC;IACxD,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,YAAY,EAAE,OAAO,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAUD,eAAO,MAAM,uBAAuB,GAClC,eAAe,mBAAmB,KACjC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAA;CAIpE,CAAC;AAEF,QAAA,MAAM,iBAAiB,GAAqC,oPAmBzD,sBAAsB,KAAG,GAAG,CAAC,OAkT/B,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileFlameGraph/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAwE,MAAM,OAAO,CAAC;AAM7F,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAO9C,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAI1C,OAAO,EAAC,mBAAmB,EAAE,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAMpE,OAAO,EAAC,gBAAgB,EAA0B,MAAM,yBAAyB,CAAC;AAIlF,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAEpE,UAAU,sBAAsB;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC;IACtC,kBAAkB,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,KAAK,IAAI,CAAC;IACxD,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,YAAY,EAAE,OAAO,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAUD,eAAO,MAAM,uBAAuB,GAClC,eAAe,mBAAmB,KACjC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAA;CAMpE,CAAC;AAEF,QAAA,MAAM,iBAAiB,GAAqC,oPAmBzD,sBAAsB,KAAG,GAAG,CAAC,OAkT/B,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
@@ -30,7 +30,9 @@ const ErrorContent = ({ errorMessage }) => {
30
30
  };
31
31
  export const validateFlameChartQuery = (profileSource) => {
32
32
  const isNonDelta = !profileSource.ProfileType().delta;
33
- const isDurationTooLong = profileSource.mergeTo - profileSource.mergeFrom > 60000;
33
+ const duration = profileSource.mergeTo - profileSource.mergeFrom;
34
+ console.log('duration of flame chart query: ', duration, 'ns');
35
+ const isDurationTooLong = duration > 60000000000n; // 60 seconds in nanoseconds
34
36
  return { isValid: !isNonDelta && !isDurationTooLong, isNonDelta, isDurationTooLong };
35
37
  };
36
38
  const ProfileFlameGraph = function ProfileFlameGraphNonMemo({ arrow, total, filtered, curPathArrow, setNewCurPathArrow, profileType, loading, error, width, isHalfScreen, metadataMappingFiles, isFlameChart = false, profileSource, isInSandwichView = false, isRenderedAsFlamegraph = false, tooltipId, maxFrameCount, isExpanded = false, }) {
@@ -21,7 +21,7 @@ interface ProfileMetricsGraphProps {
21
21
  key: string;
22
22
  value: string;
23
23
  }>) => void;
24
- onPointClick: (timestamp: number, labels: Label[], queryExpression: string, duration: number) => void;
24
+ onPointClick: (timestamp: bigint, labels: Label[], queryExpression: string, duration: number) => void;
25
25
  comparing?: boolean;
26
26
  }
27
27
  declare const ProfileMetricsGraph: ({ queryClient, queryExpression, profile, from, to, setTimeRange, addLabelMatcher, onPointClick, comparing, sumBy, sumByLoading, }: ProfileMetricsGraphProps) => JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileMetricsGraph/index.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAAC,KAAK,EAAE,kBAAkB,EAAC,MAAM,eAAe,CAAC;AACxD,OAAO,EAAC,aAAa,EAAwC,MAAM,mBAAmB,CAAC;AAIvF,OAAO,EAAyB,gBAAgB,EAAC,MAAM,IAAI,CAAC;AAK5D,UAAU,6BAA6B;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AAaD,eAAO,MAAM,wBAAwB,GAAI,aAAW,6BAA6B,KAAG,GAAG,CAAC,OAMvF,CAAC;AAEF,UAAU,wBAAwB;IAChC,WAAW,EAAE,kBAAkB,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,OAAO,CAAC;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC7C,eAAe,EAAE,CACf,MAAM,EAAE;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,GAAG,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAAC,KACvE,IAAI,CAAC;IACV,YAAY,EAAE,CACZ,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,KAAK,EAAE,EACf,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,MAAM,KACb,IAAI,CAAC;IACV,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,QAAA,MAAM,mBAAmB,GAAI,mIAY1B,wBAAwB,KAAG,GAAG,CAAC,OA4FjC,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileMetricsGraph/index.tsx"],"names":[],"mappings":"AAkBA,OAAO,EACL,KAAK,EAGL,kBAAkB,EACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,aAAa,EAId,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EAAyB,gBAAgB,EAAC,MAAM,IAAI,CAAC;AAwH5D,UAAU,6BAA6B;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AAaD,eAAO,MAAM,wBAAwB,GAAI,aAAW,6BAA6B,KAAG,GAAG,CAAC,OAMvF,CAAC;AAEF,UAAU,wBAAwB;IAChC,WAAW,EAAE,kBAAkB,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,OAAO,CAAC;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC7C,eAAe,EAAE,CACf,MAAM,EAAE;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,GAAG,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAAC,KACvE,IAAI,CAAC;IACV,YAAY,EAAE,CACZ,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,KAAK,EAAE,EACf,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,MAAM,KACb,IAAI,CAAC;IACV,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,QAAA,MAAM,mBAAmB,GAAI,mIAY1B,wBAAwB,KAAG,GAAG,CAAC,OAuWjC,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  // Copyright 2022 The Parca Authors
3
3
  // Licensed under the Apache License, Version 2.0 (the "License");
4
4
  // you may not use this file except in compliance with the License.
@@ -11,14 +11,110 @@ import { jsx as _jsx } from "react/jsx-runtime";
11
11
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
- import { useEffect } from 'react';
14
+ import { useEffect, useMemo } from 'react';
15
+ import { Icon } from '@iconify/react';
15
16
  import { AnimatePresence, motion } from 'framer-motion';
16
- import { MetricsGraphSkeleton, useParcaContext } from '@parca/components';
17
+ import { MetricsGraphSkeleton, TextWithTooltip, useParcaContext, } from '@parca/components';
17
18
  import { Query } from '@parca/parser';
18
- import { capitalizeOnlyFirstLetter } from '@parca/utilities';
19
+ import { capitalizeOnlyFirstLetter, formatDate, timePattern, valueFormatter } from '@parca/utilities';
20
+ import { MergedProfileSelection } from '..';
19
21
  import MetricsGraph from '../MetricsGraph';
20
22
  import { useMetricsGraphDimensions } from '../MetricsGraph/useMetricsGraphDimensions';
21
23
  import { useQueryRange } from './hooks/useQueryRange';
24
+ const transformUtilizationLabels = (label, utilizationMetrics) => {
25
+ if (utilizationMetrics) {
26
+ return label.replace('attributes.', '').replace('attributes_resource.', '');
27
+ }
28
+ return label;
29
+ };
30
+ const createProfileContextMenuItems = (addLabelMatcher, data, // The original MetricsSeriesPb[] data
31
+ utilizationMetrics = false) => {
32
+ return [
33
+ {
34
+ id: 'focus-on-single-series',
35
+ label: 'Focus only on this series',
36
+ icon: 'ph:star',
37
+ onClick: (closestPoint, _series) => {
38
+ if (closestPoint != null && data.length > 0 && data[closestPoint.seriesIndex] != null) {
39
+ const originalSeriesData = data[closestPoint.seriesIndex];
40
+ if (originalSeriesData.labelset?.labels != null) {
41
+ const labels = originalSeriesData.labelset.labels.filter((label) => label.name !== '__name__');
42
+ const labelsToAdd = labels.map((label) => ({
43
+ key: label.name,
44
+ value: label.value,
45
+ }));
46
+ addLabelMatcher(labelsToAdd);
47
+ }
48
+ }
49
+ },
50
+ },
51
+ {
52
+ id: 'add-to-query',
53
+ label: 'Add to query',
54
+ icon: 'material-symbols:add',
55
+ createDynamicItems: (closestPoint, _series) => {
56
+ if (closestPoint == null || data.length === 0 || data[closestPoint.seriesIndex] == null) {
57
+ return [
58
+ {
59
+ id: 'no-labels-available',
60
+ label: 'No labels available',
61
+ icon: 'ph:warning',
62
+ disabled: () => true,
63
+ onClick: () => { }, // No-op for disabled item
64
+ },
65
+ ];
66
+ }
67
+ const originalSeriesData = data[closestPoint.seriesIndex];
68
+ if (originalSeriesData.labelset?.labels == null) {
69
+ return [
70
+ {
71
+ id: 'no-labels-available',
72
+ label: 'No labels available',
73
+ icon: 'ph:warning',
74
+ disabled: () => true,
75
+ onClick: () => { }, // No-op for disabled item
76
+ },
77
+ ];
78
+ }
79
+ const labels = originalSeriesData.labelset.labels.filter((label) => label.name !== '__name__');
80
+ return labels.map((label) => ({
81
+ id: `add-label-${label.name}`,
82
+ label: (_jsx("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", children: `${transformUtilizationLabels(label.name, utilizationMetrics)}="${label.value}"` })),
83
+ onClick: () => {
84
+ addLabelMatcher({
85
+ key: label.name,
86
+ value: label.value,
87
+ });
88
+ },
89
+ }));
90
+ },
91
+ },
92
+ ];
93
+ };
94
+ const transformMetricsData = (data) => {
95
+ const series = data.reduce((agg, s) => {
96
+ if (s.labelset !== undefined) {
97
+ // Generate ID from sorted labelsets
98
+ const labels = s.labelset.labels ?? [];
99
+ const sortedLabels = labels
100
+ .filter(label => label.name !== '__name__') // Exclude __name__ from ID generation
101
+ .sort((a, b) => a.name.localeCompare(b.name));
102
+ const id = sortedLabels.map(label => `${label.name}=${label.value}`).join(',');
103
+ agg.push({
104
+ id: id !== '' ? id : 'default', // fallback to 'default' if no labels
105
+ values: s.samples.reduce((agg, d) => {
106
+ if (d.timestamp !== undefined && d.valuePerSecond !== undefined) {
107
+ const timestampMs = Number(d.timestamp.seconds) * 1000 + d.timestamp.nanos / 1000000;
108
+ agg.push([timestampMs, d.valuePerSecond]);
109
+ }
110
+ return agg;
111
+ }, []),
112
+ });
113
+ }
114
+ return agg;
115
+ }, []);
116
+ return series;
117
+ };
22
118
  const ErrorContent = ({ errorMessage }) => {
23
119
  return (_jsx("div", { className: "relative rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700", role: "alert", children: _jsx("span", { className: "block sm:inline", children: errorMessage }) }));
24
120
  };
@@ -27,7 +123,7 @@ export const ProfileMetricsEmptyState = ({ message }) => {
27
123
  };
28
124
  const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to, setTimeRange, addLabelMatcher, onPointClick, comparing = false, sumBy, sumByLoading, }) => {
29
125
  const { isLoading: metricsGraphLoading, response, error, } = useQueryRange(queryClient, queryExpression, from, to, sumBy, sumByLoading);
30
- const { onError, perf, authenticationErrorMessage, isDarkMode } = useParcaContext();
126
+ const { onError, perf, authenticationErrorMessage, isDarkMode, timezone } = useParcaContext();
31
127
  const { width, height, margin, heightStyle } = useMetricsGraphDimensions(comparing);
32
128
  useEffect(() => {
33
129
  if (error !== null) {
@@ -40,30 +136,143 @@ const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to,
40
136
  }
41
137
  perf?.markInteraction('Metrics graph render', response.series[0].samples.length);
42
138
  }, [perf, response]);
43
- const series = response?.series;
44
- const dataAvailable = series !== null && series !== undefined && series?.length > 0;
139
+ const originalSeries = response?.series;
140
+ const selectedPoint = useMemo(() => {
141
+ if (profile !== null && profile instanceof MergedProfileSelection) {
142
+ // Iterate over the series and find the series index that matches all
143
+ // labels of the profile selection. We specifically need the index
144
+ // because that's what the SeriesPoint interface expects.
145
+ const seriesIndex = originalSeries?.findIndex(s => {
146
+ return s.labelset?.labels?.every(label => {
147
+ return profile.query.matchers.some(matcher => {
148
+ return matcher.key === label.name && matcher.value === label.value;
149
+ });
150
+ });
151
+ });
152
+ // if we found a series, return the point that matches the from/to timestamp exactly (in millisecond precision)
153
+ if (seriesIndex !== undefined &&
154
+ seriesIndex !== -1 &&
155
+ originalSeries !== undefined &&
156
+ originalSeries[seriesIndex] != null) {
157
+ const series = originalSeries[seriesIndex];
158
+ const pointIndex = series.samples.findIndex(sample => {
159
+ return (sample.timestamp?.seconds === BigInt(profile.mergeFrom / 1000000000n) &&
160
+ sample.timestamp?.nanos === Number(profile.mergeFrom % 1000000000n));
161
+ });
162
+ if (pointIndex !== -1) {
163
+ return {
164
+ seriesIndex,
165
+ pointIndex,
166
+ };
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+ return null;
172
+ }, [profile, originalSeries]);
173
+ const transformedSeries = useMemo(() => {
174
+ return originalSeries != null ? transformMetricsData(originalSeries) : [];
175
+ }, [originalSeries]);
176
+ const contextMenuItems = useMemo(() => {
177
+ return originalSeries != null
178
+ ? createProfileContextMenuItems(addLabelMatcher, originalSeries)
179
+ : [];
180
+ }, [originalSeries, addLabelMatcher]);
181
+ const dataAvailable = originalSeries !== null && originalSeries !== undefined && originalSeries?.length > 0;
182
+ const { sampleUnit, sampleType, yAxisLabel, yAxisUnit } = useMemo(() => {
183
+ let sampleUnit = '';
184
+ let sampleType = '';
185
+ if (dataAvailable) {
186
+ if (originalSeries?.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)) {
187
+ sampleUnit = originalSeries[0]?.sampleType?.unit ?? '';
188
+ sampleType = originalSeries[0]?.sampleType?.type ?? '';
189
+ }
190
+ if (sampleUnit === '') {
191
+ const profileType = Query.parse(queryExpression).profileType();
192
+ sampleUnit = profileType.sampleUnit;
193
+ sampleType = profileType.sampleType;
194
+ }
195
+ }
196
+ // Calculate axis labels based on profile data
197
+ const isDeltaType = profile !== null ? profile?.query.profType.delta : false;
198
+ let yAxisLabel = sampleUnit;
199
+ let yAxisUnit = sampleUnit;
200
+ if (isDeltaType) {
201
+ if (sampleUnit === 'nanoseconds') {
202
+ if (sampleType === 'cpu') {
203
+ yAxisLabel = 'CPU Cores';
204
+ yAxisUnit = '';
205
+ }
206
+ if (sampleType === 'cuda') {
207
+ yAxisLabel = 'GPU Time';
208
+ }
209
+ }
210
+ if (sampleUnit === 'bytes') {
211
+ yAxisLabel = 'Bytes per Second';
212
+ }
213
+ }
214
+ return { sampleUnit, sampleType, yAxisLabel, yAxisUnit };
215
+ }, [dataAvailable, originalSeries, queryExpression, profile]);
45
216
  const loading = metricsGraphLoading;
217
+ // Handle errors after all hooks have been called
46
218
  if (!metricsGraphLoading && error !== null) {
47
219
  if (authenticationErrorMessage !== undefined && error.code === 'UNAUTHENTICATED') {
48
220
  return _jsx(ErrorContent, { errorMessage: authenticationErrorMessage });
49
221
  }
50
222
  return _jsx(ErrorContent, { errorMessage: capitalizeOnlyFirstLetter(error.message) });
51
223
  }
52
- let sampleUnit = '';
53
- let sampleType = '';
54
- if (dataAvailable) {
55
- if (series.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)) {
56
- sampleUnit = series[0]?.sampleType?.unit ?? '';
57
- sampleType = series[0]?.sampleType?.type ?? '';
58
- }
59
- if (sampleUnit === '') {
60
- const profileType = Query.parse(queryExpression).profileType();
61
- sampleUnit = profileType.sampleUnit;
62
- sampleType = profileType.sampleType;
63
- }
64
- }
65
- return (_jsx(AnimatePresence, { children: _jsx(motion.div, { className: "h-full w-full relative", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: loading ? (_jsx(MetricsGraphSkeleton, { heightStyle: heightStyle, isDarkMode: isDarkMode })) : dataAvailable ? (_jsx(MetricsGraph, { data: series, from: from, to: to, profile: profile, setTimeRange: setTimeRange, onSampleClick: (timestamp, _value, labels, duration) => {
66
- onPointClick(timestamp, labels, queryExpression, duration);
67
- }, addLabelMatcher: addLabelMatcher, sampleUnit: sampleUnit, sampleType: sampleType, height: height, width: width, margin: margin, sumBy: sumBy })) : (_jsx(ProfileMetricsEmptyState, { message: "No data found. Try a different query." })) }, "metrics-graph-loaded") }));
224
+ return (_jsx(AnimatePresence, { children: _jsx(motion.div, { className: "h-full w-full relative", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: loading ? (_jsx(MetricsGraphSkeleton, { heightStyle: heightStyle, isDarkMode: isDarkMode })) : dataAvailable ? (_jsx(MetricsGraph, { data: transformedSeries, from: from, to: to, setTimeRange: setTimeRange, selectedPoint: selectedPoint, onSampleClick: (closestPoint) => {
225
+ // Use original data for both series and point
226
+ if (originalSeries?.[closestPoint.seriesIndex] != null) {
227
+ const originalSeriesData = originalSeries[closestPoint.seriesIndex];
228
+ const originalPoint = originalSeriesData.samples[closestPoint.pointIndex];
229
+ if (originalPoint.timestamp != null && originalPoint.valuePerSecond !== undefined) {
230
+ const timestampNanos = originalPoint.timestamp.seconds * 1000000000n +
231
+ BigInt(originalPoint.timestamp.nanos);
232
+ onPointClick(timestampNanos, // Convert to number to match interface
233
+ originalSeriesData.labelset?.labels ?? [], queryExpression, Number(originalPoint.duration ?? 0) // Convert bigint to number
234
+ );
235
+ }
236
+ }
237
+ }, renderTooltipContent: (seriesIndex, pointIndex) => {
238
+ if (originalSeries?.[seriesIndex]?.samples?.[pointIndex] != null) {
239
+ const originalSeriesData = originalSeries[seriesIndex];
240
+ const originalPoint = originalSeriesData.samples[pointIndex];
241
+ if (originalPoint.timestamp != null && originalPoint.valuePerSecond !== undefined) {
242
+ const timestampMs = Number(originalPoint.timestamp.seconds) * 1000 +
243
+ originalPoint.timestamp.nanos / 1000000;
244
+ const labels = originalSeriesData.labelset?.labels ?? [];
245
+ const nameLabel = labels.find(e => e.name === '__name__');
246
+ const highlightedNameLabel = nameLabel ?? { name: '', value: '' };
247
+ // Calculate attributes maps for utilization metrics
248
+ const utilizationMetrics = false; // This is for profile metrics, not utilization
249
+ const attributesMap = labels
250
+ .filter(label => label.name.startsWith('attributes.') &&
251
+ !label.name.startsWith('attributes_resource.'))
252
+ .reduce((acc, label) => {
253
+ const key = label.name.replace('attributes.', '');
254
+ acc[key] = label.value;
255
+ return acc;
256
+ }, {});
257
+ const attributesResourceMap = labels
258
+ .filter(label => label.name.startsWith('attributes_resource.'))
259
+ .reduce((acc, label) => {
260
+ const key = label.name.replace('attributes_resource.', '');
261
+ acc[key] = label.value;
262
+ return acc;
263
+ }, {});
264
+ const isDeltaType = profile !== null
265
+ ? profile?.query.profType.delta
266
+ : false;
267
+ return (_jsx("div", { className: "flex flex-row", children: _jsxs("div", { className: "ml-2 mr-6", children: [_jsx("span", { className: "font-semibold", children: highlightedNameLabel.value }), _jsx("span", { className: "my-2 block text-gray-700 dark:text-gray-300", children: _jsx("table", { className: "table-auto", children: _jsxs("tbody", { children: [isDeltaType ? (_jsxs(_Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { className: "w-1/4 pr-3", children: "Per\u00A0Second" }), _jsx("td", { className: "w-3/4", children: valueFormatter(originalPoint.valuePerSecond, sampleUnit === 'nanoseconds' && sampleType === 'cpu'
268
+ ? 'CPU Cores'
269
+ : sampleUnit, 5) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Total" }), _jsx("td", { className: "w-3/4", children: valueFormatter(originalPoint.value ?? 0, sampleUnit, 2) })] })] })) : (_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Value" }), _jsx("td", { className: "w-3/4", children: valueFormatter(originalPoint.valuePerSecond, sampleUnit, 5) })] })), originalPoint.duration != null &&
270
+ Number(originalPoint.duration) > 0 && (_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Duration" }), _jsx("td", { className: "w-3/4", children: valueFormatter(Number(originalPoint.duration.toString()), 'nanoseconds', 2) })] })), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "At" }), _jsx("td", { className: "w-3/4", children: formatDate(new Date(timestampMs), timePattern(timezone), timezone) })] })] }) }) }), _jsx("span", { className: "my-2 block text-gray-500", children: utilizationMetrics ? (_jsxs(_Fragment, { children: [Object.keys(attributesResourceMap).length > 0 && (_jsx("span", { className: "text-sm font-bold text-gray-700 dark:text-white", children: "Resource Attributes" })), _jsx("span", { className: "my-2 block text-gray-500", children: Object.keys(attributesResourceMap).map(name => (_jsx("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-400", children: _jsx(TextWithTooltip, { text: `${name.replace('attributes.', '')}="${attributesResourceMap[name]}"`, maxTextLength: 48, id: `tooltip-${name}-${attributesResourceMap[name]}` }) }, name))) }), Object.keys(attributesMap).length > 0 && (_jsx("span", { className: "text-sm font-bold text-gray-700 dark:text-white", children: "Attributes" })), _jsx("span", { className: "my-2 block text-gray-500", children: Object.keys(attributesMap).map(name => (_jsx("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-400", children: _jsx(TextWithTooltip, { text: `${name.replace('attributes.', '')}="${attributesMap[name]}"`, maxTextLength: 48, id: `tooltip-${name}-${attributesMap[name]}` }) }, name))) })] })) : (_jsx(_Fragment, { children: labels
271
+ .filter((label) => label.name !== '__name__')
272
+ .map((label) => (_jsx("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-400", children: _jsx(TextWithTooltip, { text: `${label.name}="${label.value}"`, maxTextLength: 37, id: `tooltip-${label.name}` }) }, label.name))) })) }), _jsxs("div", { className: "flex w-full items-center gap-1 text-xs text-gray-500", children: [_jsx(Icon, { icon: "iconoir:mouse-button-right" }), _jsx("div", { children: "Right click to add labels to query." })] })] }) }));
273
+ }
274
+ }
275
+ return null;
276
+ }, yAxisLabel: yAxisLabel, yAxisUnit: yAxisUnit, height: height, width: width, margin: margin, contextMenuItems: contextMenuItems })) : (_jsx(ProfileMetricsEmptyState, { message: "No data found. Try a different query." })) }, "metrics-graph-loaded") }));
68
277
  };
69
278
  export default ProfileMetricsGraph;
@@ -26,10 +26,7 @@ interface MetricsGraphSectionProps {
26
26
  data: UtilizationMetricsType[];
27
27
  }>;
28
28
  utilizationMetricsLoading?: boolean;
29
- onUtilizationSeriesSelect?: (series: Array<{
30
- key: string;
31
- value: string;
32
- }>) => void;
29
+ onUtilizationSeriesSelect?: (seriesIndex: number) => void;
33
30
  }
34
31
  export declare function MetricsGraphSection({ showMetricsGraph, setDisplayHideMetricsGraphButton, heightStyle, querySelection, profileSelection, comparing, sumBy, defaultSumByLoading, queryClient, queryExpressionString, setTimeRangeSelection, selectQuery, selectProfile, query, setNewQueryExpression, utilizationMetrics, utilizationMetricsLoading, onUtilizationSeriesSelect, }: MetricsGraphSectionProps): JSX.Element;
35
32
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"MetricsGraphSection.d.ts","sourceRoot":"","sources":["../../src/ProfileSelector/MetricsGraphSection.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAQ,kBAAkB,EAAC,MAAM,eAAe,CAAC;AACxD,OAAO,EAAC,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAC,KAAK,EAAC,MAAM,eAAe,CAAC;AAEpC,OAAO,EAAyB,gBAAgB,EAAC,MAAM,IAAI,CAAC;AAK5D,OAAO,EAAC,cAAc,EAAE,KAAK,kBAAkB,IAAI,sBAAsB,EAAC,MAAM,SAAS,CAAC;AAE1F,UAAU,wBAAwB;IAChC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gCAAgC,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;IAC/B,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC1C,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACvB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,WAAW,EAAE,kBAAkB,CAAC;IAChC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACtD,WAAW,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,aAAa,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAClD,KAAK,EAAE,KAAK,CAAC;IACb,qBAAqB,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,kBAAkB,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD,kBAAkB,CAAC,EAAE,KAAK,CAAC;QACzB,IAAI,EAAE,MAAM,CAAC;QACb,iBAAiB,EAAE,MAAM,CAAC;QAC1B,IAAI,EAAE,sBAAsB,EAAE,CAAC;KAChC,CAAC,CAAC;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,yBAAyB,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAAC,KAAK,IAAI,CAAC;CACnF;AAED,wBAAgB,mBAAmB,CAAC,EAClC,gBAAgB,EAChB,gCAAgC,EAChC,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,SAAS,EACT,KAAK,EACL,mBAAmB,EACnB,WAAW,EACX,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,KAAK,EACL,qBAAqB,EACrB,kBAAkB,EAClB,yBAAyB,EACzB,yBAAyB,GAC1B,EAAE,wBAAwB,GAAG,GAAG,CAAC,OAAO,CAsMxC"}
1
+ {"version":3,"file":"MetricsGraphSection.d.ts","sourceRoot":"","sources":["../../src/ProfileSelector/MetricsGraphSection.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAQ,kBAAkB,EAAC,MAAM,eAAe,CAAC;AACxD,OAAO,EAAC,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAC,KAAK,EAAC,MAAM,eAAe,CAAC;AAEpC,OAAO,EAAyB,gBAAgB,EAAC,MAAM,IAAI,CAAC;AAK5D,OAAO,EAAC,cAAc,EAAE,KAAK,kBAAkB,IAAI,sBAAsB,EAAC,MAAM,SAAS,CAAC;AAE1F,UAAU,wBAAwB;IAChC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gCAAgC,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;IAC/B,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC1C,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACvB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,WAAW,EAAE,kBAAkB,CAAC;IAChC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACtD,WAAW,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,aAAa,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAClD,KAAK,EAAE,KAAK,CAAC;IACb,qBAAqB,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,kBAAkB,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD,kBAAkB,CAAC,EAAE,KAAK,CAAC;QACzB,IAAI,EAAE,MAAM,CAAC;QACb,iBAAiB,EAAE,MAAM,CAAC;QAC1B,IAAI,EAAE,sBAAsB,EAAE,CAAC;KAChC,CAAC,CAAC;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,yBAAyB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3D;AAED,wBAAgB,mBAAmB,CAAC,EAClC,gBAAgB,EAChB,gCAAgC,EAChC,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,SAAS,EACT,KAAK,EACL,mBAAmB,EACnB,WAAW,EACX,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,KAAK,EACL,qBAAqB,EACrB,kBAAkB,EAClB,yBAAyB,EACzB,yBAAyB,GAC1B,EAAE,wBAAwB,GAAG,GAAG,CAAC,OAAO,CA0MxC"}