@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
|
@@ -377,7 +377,7 @@ const CustomSelect: React.FC<CustomSelectProps & Record<string, any>> = ({
|
|
|
377
377
|
<RefreshButton
|
|
378
378
|
onClick={() => void handleRefetch()}
|
|
379
379
|
disabled={isRefetching}
|
|
380
|
-
title="Refresh
|
|
380
|
+
title="Refresh label values"
|
|
381
381
|
testId={TEST_IDS.LABEL_VALUE_REFRESH_BUTTON}
|
|
382
382
|
sticky={true}
|
|
383
383
|
loading={isRefetching}
|
|
@@ -11,23 +11,31 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {useCallback, useEffect, useState} from 'react';
|
|
14
|
+
import {useCallback, useEffect, useMemo, useState} from 'react';
|
|
15
15
|
|
|
16
16
|
import {Icon} from '@iconify/react';
|
|
17
17
|
import {useQueryClient} from '@tanstack/react-query';
|
|
18
18
|
import cx from 'classnames';
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import {QueryServiceClient} from '@parca/client';
|
|
21
|
+
import {useGrpcMetadata} from '@parca/components';
|
|
22
|
+
import {Query} from '@parca/parser';
|
|
21
23
|
import {TEST_IDS, testId} from '@parca/test-utils';
|
|
22
24
|
import {millisToProtoTimestamp, sanitizeLabelValue} from '@parca/utilities';
|
|
23
25
|
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {useQueryState} from '../hooks/useQueryState';
|
|
26
|
+
import {LabelProvider, useLabels} from '../contexts/SimpleMatchersLabelContext';
|
|
27
|
+
import {useUtilizationLabels} from '../contexts/UtilizationLabelsContext';
|
|
27
28
|
import Select, {type SelectItem} from './Select';
|
|
28
29
|
|
|
29
30
|
interface Props {
|
|
31
|
+
queryClient: QueryServiceClient;
|
|
32
|
+
setMatchersString: (arg: string) => void;
|
|
33
|
+
runQuery: () => void;
|
|
34
|
+
currentQuery: Query;
|
|
35
|
+
profileType: string;
|
|
30
36
|
queryBrowserRef: React.RefObject<HTMLDivElement>;
|
|
37
|
+
start?: number;
|
|
38
|
+
end?: number;
|
|
31
39
|
searchExecutedTimestamp?: number;
|
|
32
40
|
}
|
|
33
41
|
|
|
@@ -39,12 +47,22 @@ interface QueryRow {
|
|
|
39
47
|
isLoading: boolean;
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
const trimOtelPrefix = (labelName: string): string => {
|
|
51
|
+
if (labelName.startsWith('attributes_resource.')) {
|
|
52
|
+
return labelName.replace('attributes_resource.', '');
|
|
53
|
+
}
|
|
54
|
+
if (labelName.startsWith('attributes.')) {
|
|
55
|
+
return labelName.replace('attributes.', '');
|
|
56
|
+
}
|
|
57
|
+
return labelName;
|
|
58
|
+
};
|
|
59
|
+
|
|
42
60
|
export const transformLabelsForSelect = (labelNames: string[]): SelectItem[] => {
|
|
43
61
|
return labelNames.map(labelName => ({
|
|
44
62
|
key: labelName,
|
|
45
63
|
element: {
|
|
46
|
-
active: <>{
|
|
47
|
-
expanded: <>{
|
|
64
|
+
active: <>{trimOtelPrefix(labelName)}</>,
|
|
65
|
+
expanded: <>{trimOtelPrefix(labelName)}</>,
|
|
48
66
|
},
|
|
49
67
|
}));
|
|
50
68
|
};
|
|
@@ -97,31 +115,25 @@ const operatorOptions = [
|
|
|
97
115
|
];
|
|
98
116
|
|
|
99
117
|
const SimpleMatchers = ({
|
|
118
|
+
queryClient,
|
|
119
|
+
setMatchersString,
|
|
120
|
+
currentQuery,
|
|
121
|
+
profileType,
|
|
100
122
|
queryBrowserRef,
|
|
101
|
-
|
|
123
|
+
start,
|
|
124
|
+
end,
|
|
102
125
|
searchExecutedTimestamp,
|
|
103
126
|
}: Props): JSX.Element => {
|
|
127
|
+
const utilizationLabels = useUtilizationLabels();
|
|
104
128
|
const [queryRows, setQueryRows] = useState<QueryRow[]>([
|
|
105
129
|
{labelName: '', operator: '=', labelValue: '', labelValues: [], isLoading: false},
|
|
106
130
|
]);
|
|
107
131
|
const reactQueryClient = useQueryClient();
|
|
108
132
|
const metadata = useGrpcMetadata();
|
|
109
|
-
const {queryServiceClient: parcaQueryClient} = useParcaContext();
|
|
110
133
|
|
|
111
134
|
const [showAll, setShowAll] = useState(false);
|
|
112
135
|
const [isActivelyEditing, setIsActivelyEditing] = useState(false);
|
|
113
136
|
|
|
114
|
-
const {
|
|
115
|
-
labelNameMappingsForSimpleMatchers: labelNameOptions,
|
|
116
|
-
isLabelNamesLoading: labelNamesLoading,
|
|
117
|
-
refetchLabelNames,
|
|
118
|
-
suffix,
|
|
119
|
-
} = useUnifiedLabels();
|
|
120
|
-
|
|
121
|
-
const {draftSelection, setDraftMatchers, draftParsedQuery} = useQueryState({
|
|
122
|
-
suffix,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
137
|
// Reset editing mode when search is executed
|
|
126
138
|
useEffect(() => {
|
|
127
139
|
if (searchExecutedTimestamp !== undefined && searchExecutedTimestamp > 0) {
|
|
@@ -135,25 +147,18 @@ const SimpleMatchers = ({
|
|
|
135
147
|
|
|
136
148
|
const maxWidthInPixels = `max-w-[${queryBrowserRef.current?.offsetWidth.toString() as string}px]`;
|
|
137
149
|
|
|
138
|
-
const currentMatchers =
|
|
139
|
-
const profileType = draftParsedQuery?.profileType().toString();
|
|
140
|
-
const start = draftSelection.from;
|
|
141
|
-
const end = draftSelection.to;
|
|
150
|
+
const currentMatchers = currentQuery.matchersString();
|
|
142
151
|
|
|
143
152
|
const fetchLabelValues = useCallback(
|
|
144
153
|
async (labelName: string): Promise<string[]> => {
|
|
145
|
-
if (labelName == null || labelName === '') {
|
|
146
|
-
return [];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (profileType == null || profileType === '') {
|
|
154
|
+
if (labelName == null || labelName === '' || profileType == null || profileType === '') {
|
|
150
155
|
return [];
|
|
151
156
|
}
|
|
152
157
|
try {
|
|
153
158
|
const values = await reactQueryClient.fetchQuery(
|
|
154
159
|
[labelName, profileType, start, end],
|
|
155
160
|
async () => {
|
|
156
|
-
const response = await
|
|
161
|
+
const response = await queryClient.values(
|
|
157
162
|
{
|
|
158
163
|
labelName,
|
|
159
164
|
match: [],
|
|
@@ -178,7 +183,14 @@ const SimpleMatchers = ({
|
|
|
178
183
|
return [];
|
|
179
184
|
}
|
|
180
185
|
},
|
|
181
|
-
[
|
|
186
|
+
[queryClient, metadata, profileType, reactQueryClient, start, end]
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const fetchLabelValuesUtilization = useCallback(
|
|
190
|
+
async (labelName: string): Promise<string[]> => {
|
|
191
|
+
return (await utilizationLabels?.utilizationFetchLabelValues?.(labelName)) ?? [];
|
|
192
|
+
},
|
|
193
|
+
[utilizationLabels]
|
|
182
194
|
);
|
|
183
195
|
|
|
184
196
|
const updateMatchersString = useCallback(
|
|
@@ -187,11 +199,18 @@ const SimpleMatchers = ({
|
|
|
187
199
|
.filter(row => row.labelName.length > 0 && row.labelValue)
|
|
188
200
|
.map(row => `${row.labelName}${row.operator}"${row.labelValue}"`)
|
|
189
201
|
.join(',');
|
|
190
|
-
|
|
202
|
+
setMatchersString(matcherString);
|
|
191
203
|
},
|
|
192
|
-
[
|
|
204
|
+
[setMatchersString]
|
|
193
205
|
);
|
|
194
206
|
|
|
207
|
+
const {
|
|
208
|
+
labelNameOptions,
|
|
209
|
+
isLoading: labelNamesLoading,
|
|
210
|
+
refetchLabelValues,
|
|
211
|
+
refetchLabelNames,
|
|
212
|
+
} = useLabels();
|
|
213
|
+
|
|
195
214
|
// Helper to ensure selected label name is in the options (for page load before API returns)
|
|
196
215
|
const getLabelNameOptionsWithSelected = useCallback(
|
|
197
216
|
(selectedLabelName: string): typeof labelNameOptions => {
|
|
@@ -257,13 +276,20 @@ const SimpleMatchers = ({
|
|
|
257
276
|
|
|
258
277
|
const fetchLabelValuesUnified = useCallback(
|
|
259
278
|
async (labelName: string): Promise<string[]> => {
|
|
260
|
-
|
|
279
|
+
const labelType = labelNameOptions.find(option =>
|
|
280
|
+
option.values.some(e => e.key === labelName)
|
|
281
|
+
)?.type;
|
|
282
|
+
const labelValues =
|
|
283
|
+
labelType === 'gpu'
|
|
284
|
+
? await fetchLabelValuesUtilization(labelName)
|
|
285
|
+
: await fetchLabelValues(labelName);
|
|
286
|
+
return labelValues;
|
|
261
287
|
},
|
|
262
|
-
[fetchLabelValues]
|
|
288
|
+
[fetchLabelValues, fetchLabelValuesUtilization, labelNameOptions]
|
|
263
289
|
);
|
|
264
290
|
|
|
265
291
|
useEffect(() => {
|
|
266
|
-
if (currentMatchers === ''
|
|
292
|
+
if (currentMatchers === '') {
|
|
267
293
|
const defaultRow = {
|
|
268
294
|
labelName: '',
|
|
269
295
|
operator: '=',
|
|
@@ -280,7 +306,7 @@ const SimpleMatchers = ({
|
|
|
280
306
|
const fetchAndSetQueryRows = async (): Promise<void> => {
|
|
281
307
|
const newRows = await Promise.all(
|
|
282
308
|
currentMatchers.split(',').map(async matcher => {
|
|
283
|
-
const match = matcher.match(
|
|
309
|
+
const match = matcher.match(/([^=!~]+)([=!~]{1,2})(.+)/);
|
|
284
310
|
if (match === null) return null;
|
|
285
311
|
|
|
286
312
|
const [, labelName, operator, labelValue] = match;
|
|
@@ -414,13 +440,6 @@ const SimpleMatchers = ({
|
|
|
414
440
|
|
|
415
441
|
const isRowRegex = (row: QueryRow): boolean => row.operator === '=~' || row.operator === '!~';
|
|
416
442
|
|
|
417
|
-
const handleRefetchForLabelValues = useCallback(
|
|
418
|
-
async (labelName: string): Promise<void> => {
|
|
419
|
-
await fetchLabelValuesUnified(labelName);
|
|
420
|
-
},
|
|
421
|
-
[fetchLabelValuesUnified]
|
|
422
|
-
);
|
|
423
|
-
|
|
424
443
|
return (
|
|
425
444
|
<div
|
|
426
445
|
className={`flex items-center gap-3 ${maxWidthInPixels} w-full flex-wrap`}
|
|
@@ -466,7 +485,7 @@ const SimpleMatchers = ({
|
|
|
466
485
|
onButtonClick={() => handleLabelValueClick(index)}
|
|
467
486
|
editable={isRowRegex(row)}
|
|
468
487
|
{...testId(TEST_IDS.LABEL_VALUE_SELECT)}
|
|
469
|
-
refetchValues={async () => await
|
|
488
|
+
refetchValues={async () => await refetchLabelValues(row.labelName)}
|
|
470
489
|
showLoadingInButton={true}
|
|
471
490
|
/>
|
|
472
491
|
<button
|
|
@@ -511,4 +530,24 @@ const SimpleMatchers = ({
|
|
|
511
530
|
);
|
|
512
531
|
};
|
|
513
532
|
|
|
514
|
-
export default
|
|
533
|
+
export default function SimpleMathersWithProvider(props: Props): JSX.Element {
|
|
534
|
+
const labelNameFromMatchers = useMemo(() => {
|
|
535
|
+
if (props.currentQuery === undefined) return [];
|
|
536
|
+
|
|
537
|
+
const matchers = props.currentQuery.matchers;
|
|
538
|
+
|
|
539
|
+
return matchers.map(matcher => matcher.key);
|
|
540
|
+
}, [props.currentQuery]);
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
<LabelProvider
|
|
544
|
+
queryClient={props.queryClient}
|
|
545
|
+
profileType={props.profileType}
|
|
546
|
+
labelNameFromMatchers={labelNameFromMatchers}
|
|
547
|
+
start={props.start}
|
|
548
|
+
end={props.end}
|
|
549
|
+
>
|
|
550
|
+
<SimpleMatchers {...props} />
|
|
551
|
+
</LabelProvider>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
@@ -16,32 +16,40 @@ import React, {useCallback, useEffect, useRef, useState} from 'react';
|
|
|
16
16
|
import {Icon} from '@iconify/react';
|
|
17
17
|
import cx from 'classnames';
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {QueryServiceClient} from '@parca/client';
|
|
20
|
+
import {useGrpcMetadata} from '@parca/components';
|
|
21
|
+
import {Query} from '@parca/parser';
|
|
20
22
|
import {TEST_IDS, testId} from '@parca/test-utils';
|
|
21
23
|
import {millisToProtoTimestamp, sanitizeLabelValue} from '@parca/utilities';
|
|
22
24
|
|
|
23
25
|
import CustomSelect, {SelectItem} from '../SimpleMatchers/Select';
|
|
24
|
-
import {useUnifiedLabels} from '../contexts/UnifiedLabelsContext';
|
|
25
|
-
import {useQueryState} from '../hooks/useQueryState';
|
|
26
26
|
|
|
27
27
|
interface Props {
|
|
28
28
|
labelNames: string[];
|
|
29
|
+
profileType: string;
|
|
30
|
+
runQuery: () => void;
|
|
31
|
+
currentQuery: Query;
|
|
32
|
+
queryClient: QueryServiceClient;
|
|
33
|
+
setMatchersString: (arg: string) => void;
|
|
34
|
+
start?: number;
|
|
35
|
+
end?: number;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
const ViewMatchers: React.FC<Props> = ({
|
|
38
|
+
const ViewMatchers: React.FC<Props> = ({
|
|
39
|
+
labelNames,
|
|
40
|
+
profileType,
|
|
41
|
+
queryClient,
|
|
42
|
+
runQuery,
|
|
43
|
+
setMatchersString,
|
|
44
|
+
start,
|
|
45
|
+
end,
|
|
46
|
+
currentQuery,
|
|
47
|
+
}) => {
|
|
32
48
|
const [labelValuesMap, setLabelValuesMap] = useState<Record<string, string[]>>({});
|
|
33
49
|
const [isLoading, setIsLoading] = useState<Record<string, boolean>>({});
|
|
34
50
|
const metadata = useGrpcMetadata();
|
|
35
|
-
const {queryServiceClient: parcaQueryClient} = useParcaContext();
|
|
36
51
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const {draftSelection, setDraftMatchers, commitDraft, draftParsedQuery} = useQueryState({suffix});
|
|
40
|
-
|
|
41
|
-
const currentMatchers = draftParsedQuery?.matchersString();
|
|
42
|
-
const profileType = draftParsedQuery?.profileType().toString();
|
|
43
|
-
const start = draftSelection.from;
|
|
44
|
-
const end = draftSelection.to;
|
|
52
|
+
const currentMatchers = currentQuery.matchersString();
|
|
45
53
|
|
|
46
54
|
const parseCurrentMatchers = useCallback((matchersString: string): Record<string, string> => {
|
|
47
55
|
const matches = matchersString.match(/(\w+)="([^"]+)"/g);
|
|
@@ -60,15 +68,15 @@ const ViewMatchers: React.FC<Props> = ({labelNames}) => {
|
|
|
60
68
|
);
|
|
61
69
|
}, []);
|
|
62
70
|
|
|
63
|
-
const initialSelections = parseCurrentMatchers(currentMatchers
|
|
71
|
+
const initialSelections = parseCurrentMatchers(currentMatchers);
|
|
64
72
|
const selectionsRef = useRef<Record<string, string | null>>(initialSelections);
|
|
65
73
|
|
|
66
|
-
const
|
|
74
|
+
const runQueryRef = useRef(runQuery);
|
|
67
75
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
68
76
|
|
|
69
77
|
useEffect(() => {
|
|
70
|
-
|
|
71
|
-
}, [
|
|
78
|
+
runQueryRef.current = runQuery;
|
|
79
|
+
}, [runQuery]);
|
|
72
80
|
|
|
73
81
|
useEffect(() => {
|
|
74
82
|
selectionsRef.current = initialSelections;
|
|
@@ -77,7 +85,7 @@ const ViewMatchers: React.FC<Props> = ({labelNames}) => {
|
|
|
77
85
|
const fetchLabelValues = useCallback(
|
|
78
86
|
async (labelName: string): Promise<string[]> => {
|
|
79
87
|
try {
|
|
80
|
-
const response = await
|
|
88
|
+
const response = await queryClient.values(
|
|
81
89
|
{
|
|
82
90
|
labelName,
|
|
83
91
|
match: [],
|
|
@@ -97,7 +105,7 @@ const ViewMatchers: React.FC<Props> = ({labelNames}) => {
|
|
|
97
105
|
return [];
|
|
98
106
|
}
|
|
99
107
|
},
|
|
100
|
-
[
|
|
108
|
+
[queryClient, metadata, profileType, start, end]
|
|
101
109
|
);
|
|
102
110
|
|
|
103
111
|
const fetchAllLabelValues = useCallback(async (): Promise<void> => {
|
|
@@ -127,16 +135,16 @@ const ViewMatchers: React.FC<Props> = ({labelNames}) => {
|
|
|
127
135
|
.map(([ln, v]) => `${ln}="${v as string}"`);
|
|
128
136
|
|
|
129
137
|
const matcherString = matcherParts.join(',');
|
|
130
|
-
|
|
138
|
+
setMatchersString(matcherString);
|
|
131
139
|
|
|
132
140
|
if (timeoutRef.current !== null) {
|
|
133
141
|
clearTimeout(timeoutRef.current);
|
|
134
142
|
}
|
|
135
143
|
|
|
136
144
|
timeoutRef.current = setTimeout(() => {
|
|
137
|
-
|
|
145
|
+
runQueryRef.current();
|
|
138
146
|
}, 300);
|
|
139
|
-
}, [
|
|
147
|
+
}, [setMatchersString]);
|
|
140
148
|
|
|
141
149
|
const handleSelection = useCallback(
|
|
142
150
|
(labelName: string, value: string | null): void => {
|
|
@@ -0,0 +1,141 @@
|
|
|
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 React, {createContext, useContext, useMemo} from 'react';
|
|
15
|
+
|
|
16
|
+
import {QueryServiceClient} from '@parca/client';
|
|
17
|
+
|
|
18
|
+
import {useFetchUtilizationLabelValues, useLabelNames, useLabelValues} from '../MatchersInput';
|
|
19
|
+
import {useUtilizationLabels} from './UtilizationLabelsContext';
|
|
20
|
+
|
|
21
|
+
interface LabelNameMapping {
|
|
22
|
+
displayName: string;
|
|
23
|
+
fullName: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface LabelsContextType {
|
|
27
|
+
labelNames: string[];
|
|
28
|
+
labelValues: string[];
|
|
29
|
+
labelNameMappings: LabelNameMapping[];
|
|
30
|
+
isLabelNamesLoading: boolean;
|
|
31
|
+
isLabelValuesLoading: boolean;
|
|
32
|
+
currentLabelName: string | null;
|
|
33
|
+
setCurrentLabelName: (name: string | null) => void;
|
|
34
|
+
shouldHandlePrefixes: boolean;
|
|
35
|
+
refetchLabelValues: () => Promise<void>;
|
|
36
|
+
refetchLabelNames: () => Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const LabelsContext = createContext<LabelsContextType | null>(null);
|
|
40
|
+
|
|
41
|
+
interface LabelsProviderProps {
|
|
42
|
+
children: React.ReactNode;
|
|
43
|
+
queryClient: QueryServiceClient;
|
|
44
|
+
profileType: string;
|
|
45
|
+
start?: number;
|
|
46
|
+
end?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// With there being the possibility of having utilization labels, we need to be able to determine whether the labels to be used are utilization labels or profiling data labels.
|
|
50
|
+
// This context is used to determine this.
|
|
51
|
+
export function LabelsProvider({
|
|
52
|
+
children,
|
|
53
|
+
queryClient,
|
|
54
|
+
profileType,
|
|
55
|
+
start,
|
|
56
|
+
end,
|
|
57
|
+
}: LabelsProviderProps): JSX.Element {
|
|
58
|
+
const [currentLabelName, setCurrentLabelName] = React.useState<string | null>(null);
|
|
59
|
+
const utilizationLabels = useUtilizationLabels();
|
|
60
|
+
|
|
61
|
+
const {
|
|
62
|
+
result: labelNamesResponse,
|
|
63
|
+
loading: isLabelNamesLoading,
|
|
64
|
+
refetch: refetchLabelNames,
|
|
65
|
+
} = useLabelNames(queryClient, profileType, start, end);
|
|
66
|
+
|
|
67
|
+
const labelNamesFromAPI = useMemo(() => {
|
|
68
|
+
return (labelNamesResponse.error === undefined || labelNamesResponse.error == null) &&
|
|
69
|
+
labelNamesResponse !== undefined &&
|
|
70
|
+
labelNamesResponse != null
|
|
71
|
+
? labelNamesResponse.response?.labelNames.filter(e => e !== '__name__') ?? []
|
|
72
|
+
: [];
|
|
73
|
+
}, [labelNamesResponse]);
|
|
74
|
+
|
|
75
|
+
const {
|
|
76
|
+
result: labelValuesOriginal,
|
|
77
|
+
loading: isLabelValuesLoading,
|
|
78
|
+
refetch: refetchLabelValues,
|
|
79
|
+
} = useLabelValues(queryClient, currentLabelName ?? '', profileType, start, end);
|
|
80
|
+
|
|
81
|
+
const utilizationLabelValues = useFetchUtilizationLabelValues(
|
|
82
|
+
currentLabelName ?? '',
|
|
83
|
+
utilizationLabels
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const shouldHandlePrefixes = utilizationLabels?.utilizationLabelNames !== undefined;
|
|
87
|
+
|
|
88
|
+
const labelNameMappings = useMemo(() => {
|
|
89
|
+
const names = utilizationLabels?.utilizationLabelNames ?? labelNamesFromAPI;
|
|
90
|
+
return names.map(name => ({
|
|
91
|
+
displayName: name.replace(/^(attributes\.|attributes_resource\.)/, ''),
|
|
92
|
+
fullName: name,
|
|
93
|
+
}));
|
|
94
|
+
}, [labelNamesFromAPI, utilizationLabels?.utilizationLabelNames]);
|
|
95
|
+
|
|
96
|
+
const labelNames = useMemo(() => {
|
|
97
|
+
return shouldHandlePrefixes ? labelNameMappings.map(m => m.displayName) : labelNamesFromAPI;
|
|
98
|
+
}, [labelNameMappings, labelNamesFromAPI, shouldHandlePrefixes]);
|
|
99
|
+
|
|
100
|
+
const labelValues = useMemo(() => {
|
|
101
|
+
return utilizationLabels?.utilizationFetchLabelValues !== undefined
|
|
102
|
+
? utilizationLabelValues
|
|
103
|
+
: labelValuesOriginal.response;
|
|
104
|
+
}, [labelValuesOriginal, utilizationLabelValues, utilizationLabels]);
|
|
105
|
+
|
|
106
|
+
const value = useMemo(
|
|
107
|
+
() => ({
|
|
108
|
+
labelNames,
|
|
109
|
+
labelValues,
|
|
110
|
+
labelNameMappings,
|
|
111
|
+
isLabelNamesLoading,
|
|
112
|
+
isLabelValuesLoading,
|
|
113
|
+
currentLabelName,
|
|
114
|
+
setCurrentLabelName,
|
|
115
|
+
shouldHandlePrefixes,
|
|
116
|
+
refetchLabelValues,
|
|
117
|
+
refetchLabelNames,
|
|
118
|
+
}),
|
|
119
|
+
[
|
|
120
|
+
labelNames,
|
|
121
|
+
labelValues,
|
|
122
|
+
labelNameMappings,
|
|
123
|
+
isLabelNamesLoading,
|
|
124
|
+
isLabelValuesLoading,
|
|
125
|
+
currentLabelName,
|
|
126
|
+
shouldHandlePrefixes,
|
|
127
|
+
refetchLabelValues,
|
|
128
|
+
refetchLabelNames,
|
|
129
|
+
]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return <LabelsContext.Provider value={value}>{children}</LabelsContext.Provider>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function useLabels(): LabelsContextType {
|
|
136
|
+
const context = useContext(LabelsContext);
|
|
137
|
+
if (context === null) {
|
|
138
|
+
throw new Error('useLabels must be used within a LabelsProvider');
|
|
139
|
+
}
|
|
140
|
+
return context;
|
|
141
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
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 {createContext, useCallback, useContext, useMemo} from 'react';
|
|
15
|
+
|
|
16
|
+
import {useQueryClient} from '@tanstack/react-query';
|
|
17
|
+
|
|
18
|
+
import {QueryServiceClient} from '@parca/client';
|
|
19
|
+
|
|
20
|
+
import {useLabelNames} from '../MatchersInput';
|
|
21
|
+
import {transformLabelsForSelect} from '../SimpleMatchers';
|
|
22
|
+
import type {SelectItem} from '../SimpleMatchers/Select';
|
|
23
|
+
import {useUtilizationLabels} from './UtilizationLabelsContext';
|
|
24
|
+
|
|
25
|
+
interface LabelNameSection {
|
|
26
|
+
type: string;
|
|
27
|
+
values: SelectItem[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface LabelContextValue {
|
|
31
|
+
labelNameOptions: LabelNameSection[];
|
|
32
|
+
isLoading: boolean;
|
|
33
|
+
error: Error | null;
|
|
34
|
+
refetchLabelValues: (labelName?: string) => Promise<void>;
|
|
35
|
+
refetchLabelNames: () => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const LabelContext = createContext<LabelContextValue | null>(null);
|
|
39
|
+
|
|
40
|
+
interface LabelProviderProps {
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
queryClient: QueryServiceClient;
|
|
43
|
+
profileType: string;
|
|
44
|
+
labelNameFromMatchers: string[];
|
|
45
|
+
start?: number;
|
|
46
|
+
end?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// With there being the possibility of having utilization labels, we need to be able to determine whether the labels to be used are utilization labels or profiling data labels.
|
|
50
|
+
// This context is used to determine this.
|
|
51
|
+
|
|
52
|
+
export function LabelProvider({
|
|
53
|
+
children,
|
|
54
|
+
queryClient,
|
|
55
|
+
profileType,
|
|
56
|
+
labelNameFromMatchers,
|
|
57
|
+
start,
|
|
58
|
+
end,
|
|
59
|
+
}: LabelProviderProps): JSX.Element {
|
|
60
|
+
const reactQueryClient = useQueryClient();
|
|
61
|
+
const utilizationLabelResponse = useUtilizationLabels();
|
|
62
|
+
const {
|
|
63
|
+
loading,
|
|
64
|
+
result,
|
|
65
|
+
refetch: refetchLabelNamesQuery,
|
|
66
|
+
} = useLabelNames(queryClient, profileType, start, end);
|
|
67
|
+
|
|
68
|
+
const profileValues = useMemo(() => {
|
|
69
|
+
const profileLabelNames =
|
|
70
|
+
result.error != null ? [] : result.response?.labelNames.filter(e => e !== '__name__') ?? [];
|
|
71
|
+
const uniqueProfileLabelNames = Array.from(new Set(profileLabelNames));
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
labelNameOptions: uniqueProfileLabelNames,
|
|
75
|
+
isLoading: loading,
|
|
76
|
+
error: result.error ?? null,
|
|
77
|
+
};
|
|
78
|
+
}, [result, loading]);
|
|
79
|
+
|
|
80
|
+
const utilizationValues = useMemo(() => {
|
|
81
|
+
if (utilizationLabelResponse?.utilizationLabelNamesLoading === true) {
|
|
82
|
+
return {labelNameOptions: [] as string[], isLoading: true};
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
utilizationLabelResponse == null ||
|
|
86
|
+
utilizationLabelResponse.utilizationLabelNames == null
|
|
87
|
+
) {
|
|
88
|
+
return {labelNameOptions: [] as string[], isLoading: false};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const uniqueUtilizationLabelNames = Array.from(
|
|
92
|
+
new Set(utilizationLabelResponse.utilizationLabelNames)
|
|
93
|
+
);
|
|
94
|
+
return {
|
|
95
|
+
labelNameOptions: uniqueUtilizationLabelNames,
|
|
96
|
+
isLoading: utilizationLabelResponse.utilizationLabelNamesLoading,
|
|
97
|
+
};
|
|
98
|
+
}, [utilizationLabelResponse]);
|
|
99
|
+
|
|
100
|
+
const value = useMemo(() => {
|
|
101
|
+
if (
|
|
102
|
+
profileValues.error != null ||
|
|
103
|
+
profileValues.isLoading ||
|
|
104
|
+
utilizationValues.isLoading === true
|
|
105
|
+
) {
|
|
106
|
+
return {
|
|
107
|
+
labelNameOptions: [],
|
|
108
|
+
isLoading: (profileValues.isLoading || utilizationValues.isLoading) ?? false,
|
|
109
|
+
error: profileValues.error,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let nonMatchingLabels = labelNameFromMatchers.filter(
|
|
114
|
+
label => !utilizationValues.labelNameOptions.includes(label)
|
|
115
|
+
);
|
|
116
|
+
nonMatchingLabels = nonMatchingLabels.filter(
|
|
117
|
+
label => !profileValues.labelNameOptions.includes(label)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const nonMatchingLabelsSet = Array.from(new Set(nonMatchingLabels));
|
|
121
|
+
const options = [
|
|
122
|
+
{
|
|
123
|
+
type: 'cpu',
|
|
124
|
+
values: transformLabelsForSelect(profileValues.labelNameOptions),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: 'gpu',
|
|
128
|
+
values: transformLabelsForSelect(utilizationValues.labelNameOptions),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: '',
|
|
132
|
+
values: transformLabelsForSelect(nonMatchingLabelsSet),
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
labelNameOptions: options.filter(e => e.values.length > 0),
|
|
138
|
+
isLoading: false,
|
|
139
|
+
error: null,
|
|
140
|
+
};
|
|
141
|
+
}, [profileValues, utilizationValues, labelNameFromMatchers]);
|
|
142
|
+
|
|
143
|
+
const refetchLabelValues = useCallback(
|
|
144
|
+
async (labelName?: string) => {
|
|
145
|
+
await reactQueryClient.refetchQueries({
|
|
146
|
+
predicate: query => {
|
|
147
|
+
const key = query.queryKey;
|
|
148
|
+
const matchesStructure =
|
|
149
|
+
Array.isArray(key) &&
|
|
150
|
+
key.length === 4 &&
|
|
151
|
+
typeof key[0] === 'string' &&
|
|
152
|
+
key[1] === profileType;
|
|
153
|
+
|
|
154
|
+
if (!matchesStructure) return false;
|
|
155
|
+
|
|
156
|
+
if (labelName !== undefined && labelName !== '') {
|
|
157
|
+
return key[0] === labelName;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return true;
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
[reactQueryClient, profileType]
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const refetchLabelNames = useCallback(async () => {
|
|
168
|
+
await refetchLabelNamesQuery();
|
|
169
|
+
}, [refetchLabelNamesQuery]);
|
|
170
|
+
|
|
171
|
+
const contextValue = useMemo(
|
|
172
|
+
() => ({
|
|
173
|
+
...value,
|
|
174
|
+
refetchLabelValues,
|
|
175
|
+
refetchLabelNames,
|
|
176
|
+
}),
|
|
177
|
+
[value, refetchLabelValues, refetchLabelNames]
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return <LabelContext.Provider value={contextValue}>{children}</LabelContext.Provider>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function useLabels(): LabelContextValue {
|
|
184
|
+
const context = useContext(LabelContext);
|
|
185
|
+
if (context === null) {
|
|
186
|
+
throw new Error('useLabels must be used within a LabelProvider');
|
|
187
|
+
}
|
|
188
|
+
return context;
|
|
189
|
+
}
|