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