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