@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.
- package/CHANGELOG.md +4 -0
- package/dist/GraphTooltipArrow/Content.d.ts.map +1 -1
- package/dist/GraphTooltipArrow/Content.js +1 -1
- package/dist/MetricsGraph/MetricsContextMenu/index.d.ts +20 -11
- package/dist/MetricsGraph/MetricsContextMenu/index.d.ts.map +1 -1
- package/dist/MetricsGraph/MetricsContextMenu/index.js +16 -20
- package/dist/MetricsGraph/MetricsTooltip/index.d.ts +2 -8
- package/dist/MetricsGraph/MetricsTooltip/index.d.ts.map +1 -1
- package/dist/MetricsGraph/MetricsTooltip/index.js +46 -55
- package/dist/MetricsGraph/UtilizationMetrics/Throughput.d.ts +2 -5
- package/dist/MetricsGraph/UtilizationMetrics/Throughput.d.ts.map +1 -1
- package/dist/MetricsGraph/UtilizationMetrics/Throughput.js +126 -205
- package/dist/MetricsGraph/UtilizationMetrics/index.d.ts +9 -17
- package/dist/MetricsGraph/UtilizationMetrics/index.d.ts.map +1 -1
- package/dist/MetricsGraph/UtilizationMetrics/index.js +149 -208
- package/dist/MetricsGraph/index.d.ts +19 -26
- package/dist/MetricsGraph/index.d.ts.map +1 -1
- package/dist/MetricsGraph/index.js +50 -115
- package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/index.js +3 -1
- package/dist/ProfileMetricsGraph/index.d.ts +1 -1
- package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
- package/dist/ProfileMetricsGraph/index.js +232 -23
- package/dist/ProfileSelector/MetricsGraphSection.d.ts +1 -4
- package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
- package/dist/ProfileSelector/MetricsGraphSection.js +8 -4
- package/dist/ProfileSelector/index.d.ts +3 -6
- package/dist/ProfileSelector/index.d.ts.map +1 -1
- package/dist/ProfileSelector/index.js +2 -2
- package/dist/ProfileSource.d.ts +9 -6
- package/dist/ProfileSource.d.ts.map +1 -1
- package/dist/ProfileSource.js +23 -8
- package/dist/styles.css +1 -1
- package/dist/useQuery.js +1 -1
- package/package.json +6 -6
- package/src/GraphTooltipArrow/Content.tsx +2 -4
- package/src/MetricsGraph/MetricsContextMenu/index.tsx +78 -66
- package/src/MetricsGraph/MetricsTooltip/index.tsx +53 -210
- package/src/MetricsGraph/UtilizationMetrics/Throughput.tsx +242 -434
- package/src/MetricsGraph/UtilizationMetrics/index.tsx +312 -448
- package/src/MetricsGraph/index.tsx +99 -185
- package/src/ProfileFlameGraph/index.tsx +3 -1
- package/src/ProfileMetricsGraph/index.tsx +430 -37
- package/src/ProfileSelector/MetricsGraphSection.tsx +12 -8
- package/src/ProfileSelector/index.tsx +5 -5
- package/src/ProfileSource.tsx +34 -17
- 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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
}, [
|
|
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
|
|
145
|
-
if (
|
|
146
|
-
onSampleClick(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 }) },
|
|
289
|
-
? { fill: color(selected.seriesIndex
|
|
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;
|
|
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
|
|
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:
|
|
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":"
|
|
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
|
|
44
|
-
const
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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?: (
|
|
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,
|
|
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"}
|