@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
|
@@ -11,20 +11,146 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {useEffect} from 'react';
|
|
14
|
+
import {useEffect, useMemo} from 'react';
|
|
15
15
|
|
|
16
|
+
import {Icon} from '@iconify/react';
|
|
16
17
|
import {AnimatePresence, motion} from 'framer-motion';
|
|
17
18
|
|
|
18
|
-
import {
|
|
19
|
-
|
|
19
|
+
import {
|
|
20
|
+
Label,
|
|
21
|
+
MetricsSample,
|
|
22
|
+
MetricsSeries as MetricsSeriesPb,
|
|
23
|
+
QueryServiceClient,
|
|
24
|
+
} from '@parca/client';
|
|
25
|
+
import {
|
|
26
|
+
DateTimeRange,
|
|
27
|
+
MetricsGraphSkeleton,
|
|
28
|
+
TextWithTooltip,
|
|
29
|
+
useParcaContext,
|
|
30
|
+
} from '@parca/components';
|
|
20
31
|
import {Query} from '@parca/parser';
|
|
21
|
-
import {capitalizeOnlyFirstLetter} from '@parca/utilities';
|
|
32
|
+
import {capitalizeOnlyFirstLetter, formatDate, timePattern, valueFormatter} from '@parca/utilities';
|
|
22
33
|
|
|
23
34
|
import {MergedProfileSelection, ProfileSelection} from '..';
|
|
24
|
-
import MetricsGraph from '../MetricsGraph';
|
|
35
|
+
import MetricsGraph, {ContextMenuItemOrSubmenu, Series, SeriesPoint} from '../MetricsGraph';
|
|
25
36
|
import {useMetricsGraphDimensions} from '../MetricsGraph/useMetricsGraphDimensions';
|
|
26
37
|
import {useQueryRange} from './hooks/useQueryRange';
|
|
27
38
|
|
|
39
|
+
const transformUtilizationLabels = (label: string, utilizationMetrics: boolean): string => {
|
|
40
|
+
if (utilizationMetrics) {
|
|
41
|
+
return label.replace('attributes.', '').replace('attributes_resource.', '');
|
|
42
|
+
}
|
|
43
|
+
return label;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const createProfileContextMenuItems = (
|
|
47
|
+
addLabelMatcher: (
|
|
48
|
+
labels: {key: string; value: string} | Array<{key: string; value: string}>
|
|
49
|
+
) => void,
|
|
50
|
+
data: MetricsSeriesPb[], // The original MetricsSeriesPb[] data
|
|
51
|
+
utilizationMetrics = false
|
|
52
|
+
): ContextMenuItemOrSubmenu[] => {
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
id: 'focus-on-single-series',
|
|
56
|
+
label: 'Focus only on this series',
|
|
57
|
+
icon: 'ph:star',
|
|
58
|
+
onClick: (closestPoint, _series) => {
|
|
59
|
+
if (closestPoint != null && data.length > 0 && data[closestPoint.seriesIndex] != null) {
|
|
60
|
+
const originalSeriesData = data[closestPoint.seriesIndex];
|
|
61
|
+
if (originalSeriesData.labelset?.labels != null) {
|
|
62
|
+
const labels = originalSeriesData.labelset.labels.filter(
|
|
63
|
+
(label: Label) => label.name !== '__name__'
|
|
64
|
+
);
|
|
65
|
+
const labelsToAdd = labels.map((label: Label) => ({
|
|
66
|
+
key: label.name,
|
|
67
|
+
value: label.value,
|
|
68
|
+
}));
|
|
69
|
+
addLabelMatcher(labelsToAdd);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'add-to-query',
|
|
76
|
+
label: 'Add to query',
|
|
77
|
+
icon: 'material-symbols:add',
|
|
78
|
+
createDynamicItems: (closestPoint, _series) => {
|
|
79
|
+
if (closestPoint == null || data.length === 0 || data[closestPoint.seriesIndex] == null) {
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
id: 'no-labels-available',
|
|
83
|
+
label: 'No labels available',
|
|
84
|
+
icon: 'ph:warning',
|
|
85
|
+
disabled: () => true,
|
|
86
|
+
onClick: () => {}, // No-op for disabled item
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const originalSeriesData = data[closestPoint.seriesIndex];
|
|
92
|
+
if (originalSeriesData.labelset?.labels == null) {
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
id: 'no-labels-available',
|
|
96
|
+
label: 'No labels available',
|
|
97
|
+
icon: 'ph:warning',
|
|
98
|
+
disabled: () => true,
|
|
99
|
+
onClick: () => {}, // No-op for disabled item
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const labels = originalSeriesData.labelset.labels.filter(
|
|
105
|
+
(label: Label) => label.name !== '__name__'
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return labels.map((label: Label) => ({
|
|
109
|
+
id: `add-label-${label.name}`,
|
|
110
|
+
label: (
|
|
111
|
+
<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">
|
|
112
|
+
{`${transformUtilizationLabels(label.name, utilizationMetrics)}="${label.value}"`}
|
|
113
|
+
</div>
|
|
114
|
+
),
|
|
115
|
+
onClick: () => {
|
|
116
|
+
addLabelMatcher({
|
|
117
|
+
key: label.name,
|
|
118
|
+
value: label.value,
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const transformMetricsData = (data: MetricsSeriesPb[]): Series[] => {
|
|
128
|
+
const series = data.reduce<Series[]>((agg: Series[], s: MetricsSeriesPb) => {
|
|
129
|
+
if (s.labelset !== undefined) {
|
|
130
|
+
// Generate ID from sorted labelsets
|
|
131
|
+
const labels = s.labelset.labels ?? [];
|
|
132
|
+
const sortedLabels = labels
|
|
133
|
+
.filter(label => label.name !== '__name__') // Exclude __name__ from ID generation
|
|
134
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
135
|
+
const id = sortedLabels.map(label => `${label.name}=${label.value}`).join(',');
|
|
136
|
+
|
|
137
|
+
agg.push({
|
|
138
|
+
id: id !== '' ? id : 'default', // fallback to 'default' if no labels
|
|
139
|
+
values: s.samples.reduce<Array<[number, number]>>((agg, d: MetricsSample) => {
|
|
140
|
+
if (d.timestamp !== undefined && d.valuePerSecond !== undefined) {
|
|
141
|
+
const timestampMs = Number(d.timestamp.seconds) * 1000 + d.timestamp.nanos / 1_000_000;
|
|
142
|
+
agg.push([timestampMs, d.valuePerSecond]);
|
|
143
|
+
}
|
|
144
|
+
return agg;
|
|
145
|
+
}, []),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return agg;
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
return series;
|
|
152
|
+
};
|
|
153
|
+
|
|
28
154
|
interface ProfileMetricsEmptyStateProps {
|
|
29
155
|
message: string;
|
|
30
156
|
}
|
|
@@ -61,7 +187,7 @@ interface ProfileMetricsGraphProps {
|
|
|
61
187
|
labels: {key: string; value: string} | Array<{key: string; value: string}>
|
|
62
188
|
) => void;
|
|
63
189
|
onPointClick: (
|
|
64
|
-
timestamp:
|
|
190
|
+
timestamp: bigint,
|
|
65
191
|
labels: Label[],
|
|
66
192
|
queryExpression: string,
|
|
67
193
|
duration: number
|
|
@@ -87,7 +213,7 @@ const ProfileMetricsGraph = ({
|
|
|
87
213
|
response,
|
|
88
214
|
error,
|
|
89
215
|
} = useQueryRange(queryClient, queryExpression, from, to, sumBy, sumByLoading);
|
|
90
|
-
const {onError, perf, authenticationErrorMessage, isDarkMode} = useParcaContext();
|
|
216
|
+
const {onError, perf, authenticationErrorMessage, isDarkMode, timezone} = useParcaContext();
|
|
91
217
|
const {width, height, margin, heightStyle} = useMetricsGraphDimensions(comparing);
|
|
92
218
|
|
|
93
219
|
useEffect(() => {
|
|
@@ -104,12 +230,107 @@ const ProfileMetricsGraph = ({
|
|
|
104
230
|
perf?.markInteraction('Metrics graph render', response.series[0].samples.length);
|
|
105
231
|
}, [perf, response]);
|
|
106
232
|
|
|
107
|
-
const
|
|
233
|
+
const originalSeries = response?.series;
|
|
234
|
+
|
|
235
|
+
const selectedPoint = useMemo((): SeriesPoint | null => {
|
|
236
|
+
if (profile !== null && profile instanceof MergedProfileSelection) {
|
|
237
|
+
// Iterate over the series and find the series index that matches all
|
|
238
|
+
// labels of the profile selection. We specifically need the index
|
|
239
|
+
// because that's what the SeriesPoint interface expects.
|
|
240
|
+
const seriesIndex = originalSeries?.findIndex(s => {
|
|
241
|
+
return s.labelset?.labels?.every(label => {
|
|
242
|
+
return profile.query.matchers.some(matcher => {
|
|
243
|
+
return matcher.key === label.name && matcher.value === label.value;
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// if we found a series, return the point that matches the from/to timestamp exactly (in millisecond precision)
|
|
249
|
+
if (
|
|
250
|
+
seriesIndex !== undefined &&
|
|
251
|
+
seriesIndex !== -1 &&
|
|
252
|
+
originalSeries !== undefined &&
|
|
253
|
+
originalSeries[seriesIndex] != null
|
|
254
|
+
) {
|
|
255
|
+
const series = originalSeries[seriesIndex];
|
|
256
|
+
const pointIndex = series.samples.findIndex(sample => {
|
|
257
|
+
return (
|
|
258
|
+
sample.timestamp?.seconds === BigInt(profile.mergeFrom / 1_000_000_000n) &&
|
|
259
|
+
sample.timestamp?.nanos === Number(profile.mergeFrom % 1_000_000_000n)
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (pointIndex !== -1) {
|
|
264
|
+
return {
|
|
265
|
+
seriesIndex,
|
|
266
|
+
pointIndex,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return null;
|
|
274
|
+
}, [profile, originalSeries]);
|
|
108
275
|
|
|
109
|
-
const
|
|
276
|
+
const transformedSeries = useMemo(() => {
|
|
277
|
+
return originalSeries != null ? transformMetricsData(originalSeries) : [];
|
|
278
|
+
}, [originalSeries]);
|
|
279
|
+
|
|
280
|
+
const contextMenuItems = useMemo(() => {
|
|
281
|
+
return originalSeries != null
|
|
282
|
+
? createProfileContextMenuItems(addLabelMatcher, originalSeries)
|
|
283
|
+
: [];
|
|
284
|
+
}, [originalSeries, addLabelMatcher]);
|
|
285
|
+
|
|
286
|
+
const dataAvailable =
|
|
287
|
+
originalSeries !== null && originalSeries !== undefined && originalSeries?.length > 0;
|
|
288
|
+
|
|
289
|
+
const {sampleUnit, sampleType, yAxisLabel, yAxisUnit} = useMemo(() => {
|
|
290
|
+
let sampleUnit = '';
|
|
291
|
+
let sampleType = '';
|
|
292
|
+
|
|
293
|
+
if (dataAvailable) {
|
|
294
|
+
if (
|
|
295
|
+
originalSeries?.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)
|
|
296
|
+
) {
|
|
297
|
+
sampleUnit = originalSeries[0]?.sampleType?.unit ?? '';
|
|
298
|
+
sampleType = originalSeries[0]?.sampleType?.type ?? '';
|
|
299
|
+
}
|
|
300
|
+
if (sampleUnit === '') {
|
|
301
|
+
const profileType = Query.parse(queryExpression).profileType();
|
|
302
|
+
sampleUnit = profileType.sampleUnit;
|
|
303
|
+
sampleType = profileType.sampleType;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Calculate axis labels based on profile data
|
|
308
|
+
const isDeltaType =
|
|
309
|
+
profile !== null ? (profile as MergedProfileSelection)?.query.profType.delta : false;
|
|
310
|
+
let yAxisLabel = sampleUnit;
|
|
311
|
+
let yAxisUnit = sampleUnit;
|
|
312
|
+
|
|
313
|
+
if (isDeltaType) {
|
|
314
|
+
if (sampleUnit === 'nanoseconds') {
|
|
315
|
+
if (sampleType === 'cpu') {
|
|
316
|
+
yAxisLabel = 'CPU Cores';
|
|
317
|
+
yAxisUnit = '';
|
|
318
|
+
}
|
|
319
|
+
if (sampleType === 'cuda') {
|
|
320
|
+
yAxisLabel = 'GPU Time';
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (sampleUnit === 'bytes') {
|
|
324
|
+
yAxisLabel = 'Bytes per Second';
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {sampleUnit, sampleType, yAxisLabel, yAxisUnit};
|
|
329
|
+
}, [dataAvailable, originalSeries, queryExpression, profile]);
|
|
110
330
|
|
|
111
331
|
const loading = metricsGraphLoading;
|
|
112
332
|
|
|
333
|
+
// Handle errors after all hooks have been called
|
|
113
334
|
if (!metricsGraphLoading && error !== null) {
|
|
114
335
|
if (authenticationErrorMessage !== undefined && error.code === 'UNAUTHENTICATED') {
|
|
115
336
|
return <ErrorContent errorMessage={authenticationErrorMessage} />;
|
|
@@ -118,21 +339,6 @@ const ProfileMetricsGraph = ({
|
|
|
118
339
|
return <ErrorContent errorMessage={capitalizeOnlyFirstLetter(error.message)} />;
|
|
119
340
|
}
|
|
120
341
|
|
|
121
|
-
let sampleUnit = '';
|
|
122
|
-
let sampleType = '';
|
|
123
|
-
|
|
124
|
-
if (dataAvailable) {
|
|
125
|
-
if (series.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)) {
|
|
126
|
-
sampleUnit = series[0]?.sampleType?.unit ?? '';
|
|
127
|
-
sampleType = series[0]?.sampleType?.type ?? '';
|
|
128
|
-
}
|
|
129
|
-
if (sampleUnit === '') {
|
|
130
|
-
const profileType = Query.parse(queryExpression).profileType();
|
|
131
|
-
sampleUnit = profileType.sampleUnit;
|
|
132
|
-
sampleType = profileType.sampleType;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
342
|
return (
|
|
137
343
|
<AnimatePresence>
|
|
138
344
|
<motion.div
|
|
@@ -146,26 +352,213 @@ const ProfileMetricsGraph = ({
|
|
|
146
352
|
<MetricsGraphSkeleton heightStyle={heightStyle} isDarkMode={isDarkMode} />
|
|
147
353
|
) : dataAvailable ? (
|
|
148
354
|
<MetricsGraph
|
|
149
|
-
data={
|
|
355
|
+
data={transformedSeries}
|
|
150
356
|
from={from}
|
|
151
357
|
to={to}
|
|
152
|
-
profile={profile as MergedProfileSelection}
|
|
153
358
|
setTimeRange={setTimeRange}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
359
|
+
selectedPoint={selectedPoint}
|
|
360
|
+
onSampleClick={(closestPoint: SeriesPoint): void => {
|
|
361
|
+
// Use original data for both series and point
|
|
362
|
+
if (originalSeries?.[closestPoint.seriesIndex] != null) {
|
|
363
|
+
const originalSeriesData = originalSeries[closestPoint.seriesIndex];
|
|
364
|
+
const originalPoint = originalSeriesData.samples[closestPoint.pointIndex];
|
|
365
|
+
if (originalPoint.timestamp != null && originalPoint.valuePerSecond !== undefined) {
|
|
366
|
+
const timestampNanos =
|
|
367
|
+
originalPoint.timestamp.seconds * 1_000_000_000n +
|
|
368
|
+
BigInt(originalPoint.timestamp.nanos);
|
|
369
|
+
onPointClick(
|
|
370
|
+
timestampNanos, // Convert to number to match interface
|
|
371
|
+
originalSeriesData.labelset?.labels ?? [],
|
|
372
|
+
queryExpression,
|
|
373
|
+
Number(originalPoint.duration ?? 0) // Convert bigint to number
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}}
|
|
378
|
+
renderTooltipContent={(seriesIndex: number, pointIndex: number) => {
|
|
379
|
+
if (originalSeries?.[seriesIndex]?.samples?.[pointIndex] != null) {
|
|
380
|
+
const originalSeriesData = originalSeries[seriesIndex];
|
|
381
|
+
const originalPoint = originalSeriesData.samples[pointIndex];
|
|
382
|
+
|
|
383
|
+
if (originalPoint.timestamp != null && originalPoint.valuePerSecond !== undefined) {
|
|
384
|
+
const timestampMs =
|
|
385
|
+
Number(originalPoint.timestamp.seconds) * 1000 +
|
|
386
|
+
originalPoint.timestamp.nanos / 1_000_000;
|
|
387
|
+
const labels = originalSeriesData.labelset?.labels ?? [];
|
|
388
|
+
const nameLabel = labels.find(e => e.name === '__name__');
|
|
389
|
+
const highlightedNameLabel = nameLabel ?? {name: '', value: ''};
|
|
390
|
+
|
|
391
|
+
// Calculate attributes maps for utilization metrics
|
|
392
|
+
const utilizationMetrics = false; // This is for profile metrics, not utilization
|
|
393
|
+
const attributesMap = labels
|
|
394
|
+
.filter(
|
|
395
|
+
label =>
|
|
396
|
+
label.name.startsWith('attributes.') &&
|
|
397
|
+
!label.name.startsWith('attributes_resource.')
|
|
398
|
+
)
|
|
399
|
+
.reduce<Record<string, string>>((acc, label) => {
|
|
400
|
+
const key = label.name.replace('attributes.', '');
|
|
401
|
+
acc[key] = label.value;
|
|
402
|
+
return acc;
|
|
403
|
+
}, {});
|
|
404
|
+
|
|
405
|
+
const attributesResourceMap = labels
|
|
406
|
+
.filter(label => label.name.startsWith('attributes_resource.'))
|
|
407
|
+
.reduce<Record<string, string>>((acc, label) => {
|
|
408
|
+
const key = label.name.replace('attributes_resource.', '');
|
|
409
|
+
acc[key] = label.value;
|
|
410
|
+
return acc;
|
|
411
|
+
}, {});
|
|
412
|
+
|
|
413
|
+
const isDeltaType =
|
|
414
|
+
profile !== null
|
|
415
|
+
? (profile as MergedProfileSelection)?.query.profType.delta
|
|
416
|
+
: false;
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<div className="flex flex-row">
|
|
420
|
+
<div className="ml-2 mr-6">
|
|
421
|
+
<span className="font-semibold">{highlightedNameLabel.value}</span>
|
|
422
|
+
<span className="my-2 block text-gray-700 dark:text-gray-300">
|
|
423
|
+
<table className="table-auto">
|
|
424
|
+
<tbody>
|
|
425
|
+
{isDeltaType ? (
|
|
426
|
+
<>
|
|
427
|
+
<tr>
|
|
428
|
+
<td className="w-1/4 pr-3">Per Second</td>
|
|
429
|
+
<td className="w-3/4">
|
|
430
|
+
{valueFormatter(
|
|
431
|
+
originalPoint.valuePerSecond,
|
|
432
|
+
sampleUnit === 'nanoseconds' && sampleType === 'cpu'
|
|
433
|
+
? 'CPU Cores'
|
|
434
|
+
: sampleUnit,
|
|
435
|
+
5
|
|
436
|
+
)}
|
|
437
|
+
</td>
|
|
438
|
+
</tr>
|
|
439
|
+
<tr>
|
|
440
|
+
<td className="w-1/4">Total</td>
|
|
441
|
+
<td className="w-3/4">
|
|
442
|
+
{valueFormatter(originalPoint.value ?? 0, sampleUnit, 2)}
|
|
443
|
+
</td>
|
|
444
|
+
</tr>
|
|
445
|
+
</>
|
|
446
|
+
) : (
|
|
447
|
+
<tr>
|
|
448
|
+
<td className="w-1/4">Value</td>
|
|
449
|
+
<td className="w-3/4">
|
|
450
|
+
{valueFormatter(originalPoint.valuePerSecond, sampleUnit, 5)}
|
|
451
|
+
</td>
|
|
452
|
+
</tr>
|
|
453
|
+
)}
|
|
454
|
+
{originalPoint.duration != null &&
|
|
455
|
+
Number(originalPoint.duration) > 0 && (
|
|
456
|
+
<tr>
|
|
457
|
+
<td className="w-1/4">Duration</td>
|
|
458
|
+
<td className="w-3/4">
|
|
459
|
+
{valueFormatter(
|
|
460
|
+
Number(originalPoint.duration.toString()),
|
|
461
|
+
'nanoseconds',
|
|
462
|
+
2
|
|
463
|
+
)}
|
|
464
|
+
</td>
|
|
465
|
+
</tr>
|
|
466
|
+
)}
|
|
467
|
+
<tr>
|
|
468
|
+
<td className="w-1/4">At</td>
|
|
469
|
+
<td className="w-3/4">
|
|
470
|
+
{formatDate(
|
|
471
|
+
new Date(timestampMs),
|
|
472
|
+
timePattern(timezone as string),
|
|
473
|
+
timezone
|
|
474
|
+
)}
|
|
475
|
+
</td>
|
|
476
|
+
</tr>
|
|
477
|
+
</tbody>
|
|
478
|
+
</table>
|
|
479
|
+
</span>
|
|
480
|
+
<span className="my-2 block text-gray-500">
|
|
481
|
+
{utilizationMetrics ? (
|
|
482
|
+
<>
|
|
483
|
+
{Object.keys(attributesResourceMap).length > 0 && (
|
|
484
|
+
<span className="text-sm font-bold text-gray-700 dark:text-white">
|
|
485
|
+
Resource Attributes
|
|
486
|
+
</span>
|
|
487
|
+
)}
|
|
488
|
+
<span className="my-2 block text-gray-500">
|
|
489
|
+
{Object.keys(attributesResourceMap).map(name => (
|
|
490
|
+
<div
|
|
491
|
+
key={name}
|
|
492
|
+
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"
|
|
493
|
+
>
|
|
494
|
+
<TextWithTooltip
|
|
495
|
+
text={`${name.replace('attributes.', '')}="${
|
|
496
|
+
attributesResourceMap[name]
|
|
497
|
+
}"`}
|
|
498
|
+
maxTextLength={48}
|
|
499
|
+
id={`tooltip-${name}-${attributesResourceMap[name]}`}
|
|
500
|
+
/>
|
|
501
|
+
</div>
|
|
502
|
+
))}
|
|
503
|
+
</span>
|
|
504
|
+
{Object.keys(attributesMap).length > 0 && (
|
|
505
|
+
<span className="text-sm font-bold text-gray-700 dark:text-white">
|
|
506
|
+
Attributes
|
|
507
|
+
</span>
|
|
508
|
+
)}
|
|
509
|
+
<span className="my-2 block text-gray-500">
|
|
510
|
+
{Object.keys(attributesMap).map(name => (
|
|
511
|
+
<div
|
|
512
|
+
key={name}
|
|
513
|
+
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"
|
|
514
|
+
>
|
|
515
|
+
<TextWithTooltip
|
|
516
|
+
text={`${name.replace('attributes.', '')}="${
|
|
517
|
+
attributesMap[name]
|
|
518
|
+
}"`}
|
|
519
|
+
maxTextLength={48}
|
|
520
|
+
id={`tooltip-${name}-${attributesMap[name]}`}
|
|
521
|
+
/>
|
|
522
|
+
</div>
|
|
523
|
+
))}
|
|
524
|
+
</span>
|
|
525
|
+
</>
|
|
526
|
+
) : (
|
|
527
|
+
<>
|
|
528
|
+
{labels
|
|
529
|
+
.filter((label: Label) => label.name !== '__name__')
|
|
530
|
+
.map((label: Label) => (
|
|
531
|
+
<div
|
|
532
|
+
key={label.name}
|
|
533
|
+
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"
|
|
534
|
+
>
|
|
535
|
+
<TextWithTooltip
|
|
536
|
+
text={`${label.name}="${label.value}"`}
|
|
537
|
+
maxTextLength={37}
|
|
538
|
+
id={`tooltip-${label.name}`}
|
|
539
|
+
/>
|
|
540
|
+
</div>
|
|
541
|
+
))}
|
|
542
|
+
</>
|
|
543
|
+
)}
|
|
544
|
+
</span>
|
|
545
|
+
<div className="flex w-full items-center gap-1 text-xs text-gray-500">
|
|
546
|
+
<Icon icon="iconoir:mouse-button-right" />
|
|
547
|
+
<div>Right click to add labels to query.</div>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return null;
|
|
161
555
|
}}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
sampleType={sampleType}
|
|
556
|
+
yAxisLabel={yAxisLabel}
|
|
557
|
+
yAxisUnit={yAxisUnit}
|
|
165
558
|
height={height}
|
|
166
559
|
width={width}
|
|
167
560
|
margin={margin}
|
|
168
|
-
|
|
561
|
+
contextMenuItems={contextMenuItems}
|
|
169
562
|
/>
|
|
170
563
|
) : (
|
|
171
564
|
<ProfileMetricsEmptyState message="No data found. Try a different query." />
|
|
@@ -47,7 +47,7 @@ interface MetricsGraphSectionProps {
|
|
|
47
47
|
data: UtilizationMetricsType[];
|
|
48
48
|
}>;
|
|
49
49
|
utilizationMetricsLoading?: boolean;
|
|
50
|
-
onUtilizationSeriesSelect?: (
|
|
50
|
+
onUtilizationSeriesSelect?: (seriesIndex: number) => void;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function MetricsGraphSection({
|
|
@@ -123,7 +123,7 @@ export function MetricsGraphSection({
|
|
|
123
123
|
};
|
|
124
124
|
|
|
125
125
|
const handlePointClick = (
|
|
126
|
-
timestamp:
|
|
126
|
+
timestamp: bigint,
|
|
127
127
|
labels: Label[],
|
|
128
128
|
queryExpression: string,
|
|
129
129
|
duration: number
|
|
@@ -136,9 +136,8 @@ export function MetricsGraphSection({
|
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
const durationInMilliseconds = duration / 1000000; // duration is in nanoseconds
|
|
140
139
|
const mergeFrom = timestamp;
|
|
141
|
-
const mergeTo = query.profileType().delta ? mergeFrom +
|
|
140
|
+
const mergeTo = query.profileType().delta ? mergeFrom + BigInt(duration) : mergeFrom;
|
|
142
141
|
|
|
143
142
|
resetStateOnSeriesChange(); // reset some state when a new series is selected
|
|
144
143
|
selectProfile(new MergedProfileSelection(mergeFrom, mergeTo, query));
|
|
@@ -175,14 +174,19 @@ export function MetricsGraphSection({
|
|
|
175
174
|
<UtilizationMetricsGraph
|
|
176
175
|
key={name}
|
|
177
176
|
data={data}
|
|
178
|
-
addLabelMatcher={addLabelMatcher}
|
|
179
177
|
setTimeRange={handleTimeRangeChange}
|
|
180
178
|
utilizationMetricsLoading={utilizationMetricsLoading}
|
|
181
|
-
name={name}
|
|
182
179
|
humanReadableName={humanReadableName}
|
|
183
180
|
from={querySelection.from}
|
|
184
181
|
to={querySelection.to}
|
|
185
|
-
|
|
182
|
+
yAxisUnit="percentage"
|
|
183
|
+
addLabelMatcher={addLabelMatcher}
|
|
184
|
+
onSeriesClick={seriesIndex => {
|
|
185
|
+
// For generic UtilizationMetrics, just pass the series index
|
|
186
|
+
if (onUtilizationSeriesSelect != null) {
|
|
187
|
+
onUtilizationSeriesSelect(seriesIndex);
|
|
188
|
+
}
|
|
189
|
+
}}
|
|
186
190
|
/>
|
|
187
191
|
</>
|
|
188
192
|
);
|
|
@@ -207,7 +211,7 @@ export function MetricsGraphSection({
|
|
|
207
211
|
to={querySelection.to}
|
|
208
212
|
utilizationMetricsLoading={utilizationMetricsLoading}
|
|
209
213
|
selectedSeries={undefined}
|
|
210
|
-
|
|
214
|
+
onSeriesClick={onUtilizationSeriesSelect}
|
|
211
215
|
/>
|
|
212
216
|
)}
|
|
213
217
|
</div>
|
|
@@ -42,8 +42,8 @@ export interface QuerySelection {
|
|
|
42
42
|
to: number;
|
|
43
43
|
timeSelection: string;
|
|
44
44
|
sumBy?: string[];
|
|
45
|
-
mergeFrom?:
|
|
46
|
-
mergeTo?:
|
|
45
|
+
mergeFrom?: string;
|
|
46
|
+
mergeTo?: string;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
interface ProfileSelectorFeatures {
|
|
@@ -94,7 +94,7 @@ interface ProfileSelectorProps extends ProfileSelectorFeatures {
|
|
|
94
94
|
}>;
|
|
95
95
|
utilizationMetricsLoading?: boolean;
|
|
96
96
|
utilizationLabels?: UtilizationLabels;
|
|
97
|
-
onUtilizationSeriesSelect?: (
|
|
97
|
+
onUtilizationSeriesSelect?: (seriesIndex: number) => void;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export interface IProfileTypesResult {
|
|
@@ -237,8 +237,8 @@ const ProfileSelector = ({
|
|
|
237
237
|
const to = timeRangeSelection.getToMs(updateTs);
|
|
238
238
|
const mergeParams = delta
|
|
239
239
|
? {
|
|
240
|
-
mergeFrom: from,
|
|
241
|
-
mergeTo: to,
|
|
240
|
+
mergeFrom: (BigInt(from) * 1_000_000n).toString(),
|
|
241
|
+
mergeTo: (BigInt(to) * 1_000_000n).toString(),
|
|
242
242
|
}
|
|
243
243
|
: {};
|
|
244
244
|
|