@parca/profile 0.19.23 → 0.19.25
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/ProfileView/components/ColorStackLegend.d.ts.map +1 -1
- package/dist/ProfileView/components/ColorStackLegend.js +0 -1
- package/dist/ProfileView/components/DashboardItems/index.d.ts +3 -2
- package/dist/ProfileView/components/DashboardItems/index.d.ts.map +1 -1
- package/dist/ProfileView/components/DashboardItems/index.js +2 -2
- package/dist/ProfileView/components/ProfileFilters/filterPresets.d.ts +11 -0
- package/dist/ProfileView/components/ProfileFilters/filterPresets.d.ts.map +1 -0
- package/dist/ProfileView/components/ProfileFilters/filterPresets.js +64 -0
- package/dist/ProfileView/components/ProfileFilters/index.d.ts.map +1 -1
- package/dist/ProfileView/components/ProfileFilters/index.js +54 -9
- package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts +2 -0
- package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts.map +1 -1
- package/dist/ProfileView/components/ProfileFilters/useProfileFilters.js +58 -4
- package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
- package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +22 -2
- package/dist/ProfileView/index.d.ts +1 -1
- package/dist/ProfileView/index.d.ts.map +1 -1
- package/dist/ProfileView/index.js +2 -1
- package/dist/ProfileView/types/visualization.d.ts +6 -10
- package/dist/ProfileView/types/visualization.d.ts.map +1 -1
- package/dist/ProfileViewWithData.d.ts.map +1 -1
- package/dist/ProfileViewWithData.js +52 -22
- package/dist/Sandwich/components/CalleesSection.d.ts +3 -12
- package/dist/Sandwich/components/CalleesSection.d.ts.map +1 -1
- package/dist/Sandwich/components/CalleesSection.js +2 -4
- package/dist/Sandwich/components/CallersSection.d.ts +3 -13
- package/dist/Sandwich/components/CallersSection.d.ts.map +1 -1
- package/dist/Sandwich/components/CallersSection.js +5 -8
- package/dist/Sandwich/index.d.ts +2 -10
- package/dist/Sandwich/index.d.ts.map +1 -1
- package/dist/Sandwich/index.js +5 -103
- package/dist/styles.css +1 -1
- package/package.json +6 -6
- package/src/ProfileView/components/ColorStackLegend.tsx +0 -2
- package/src/ProfileView/components/DashboardItems/index.tsx +4 -12
- package/src/ProfileView/components/ProfileFilters/filterPresets.ts +76 -0
- package/src/ProfileView/components/ProfileFilters/index.tsx +65 -12
- package/src/ProfileView/components/ProfileFilters/useProfileFilters.ts +67 -6
- package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +27 -2
- package/src/ProfileView/index.tsx +2 -0
- package/src/ProfileView/types/visualization.ts +7 -18
- package/src/ProfileViewWithData.tsx +65 -30
- package/src/Sandwich/components/CalleesSection.tsx +10 -28
- package/src/Sandwich/components/CallersSection.tsx +13 -34
- package/src/Sandwich/index.tsx +8 -170
|
@@ -24,6 +24,7 @@ import {SourceView} from '../../../SourceView';
|
|
|
24
24
|
import {Table} from '../../../Table';
|
|
25
25
|
import type {
|
|
26
26
|
FlamegraphData,
|
|
27
|
+
SandwichData,
|
|
27
28
|
SourceData,
|
|
28
29
|
TopTableData,
|
|
29
30
|
VisualizationType,
|
|
@@ -36,6 +37,7 @@ interface GetDashboardItemProps {
|
|
|
36
37
|
flamegraphData: FlamegraphData;
|
|
37
38
|
flamechartData: FlamegraphData;
|
|
38
39
|
topTableData?: TopTableData;
|
|
40
|
+
sandwichData: SandwichData;
|
|
39
41
|
sourceData?: SourceData;
|
|
40
42
|
profileSource: ProfileSource;
|
|
41
43
|
total: bigint;
|
|
@@ -58,13 +60,13 @@ export const getDashboardItem = ({
|
|
|
58
60
|
flamechartData,
|
|
59
61
|
topTableData,
|
|
60
62
|
sourceData,
|
|
63
|
+
sandwichData,
|
|
61
64
|
profileSource,
|
|
62
65
|
total,
|
|
63
66
|
filtered,
|
|
64
67
|
curPathArrow,
|
|
65
68
|
setNewCurPathArrow,
|
|
66
69
|
perf,
|
|
67
|
-
queryClient,
|
|
68
70
|
}: GetDashboardItemProps): JSX.Element => {
|
|
69
71
|
switch (type) {
|
|
70
72
|
case 'flamegraph':
|
|
@@ -142,17 +144,7 @@ export const getDashboardItem = ({
|
|
|
142
144
|
);
|
|
143
145
|
case 'sandwich':
|
|
144
146
|
return topTableData != null ? (
|
|
145
|
-
<Sandwich
|
|
146
|
-
total={total}
|
|
147
|
-
filtered={filtered}
|
|
148
|
-
loading={topTableData.loading}
|
|
149
|
-
data={topTableData.arrow?.record}
|
|
150
|
-
unit={topTableData.unit}
|
|
151
|
-
profileType={profileSource?.ProfileType()}
|
|
152
|
-
metadataMappingFiles={flamegraphData.metadataMappingFiles}
|
|
153
|
-
profileSource={profileSource}
|
|
154
|
-
queryClient={queryClient}
|
|
155
|
-
/>
|
|
147
|
+
<Sandwich profileSource={profileSource} sandwichData={sandwichData} />
|
|
156
148
|
) : (
|
|
157
149
|
<></>
|
|
158
150
|
);
|
|
@@ -0,0 +1,76 @@
|
|
|
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 type {ProfileFilter} from '@parca/store';
|
|
15
|
+
|
|
16
|
+
export interface FilterPreset {
|
|
17
|
+
key: string;
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
filters: Array<Omit<ProfileFilter, 'id'>>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const filterPresets: FilterPreset[] = [
|
|
24
|
+
{
|
|
25
|
+
key: 'go_runtime_expected_off_cpu',
|
|
26
|
+
name: 'Go Runtime Expected Off-CPU',
|
|
27
|
+
description: 'Excludes expected Go runtime blocking functions',
|
|
28
|
+
filters: [
|
|
29
|
+
{
|
|
30
|
+
type: 'stack',
|
|
31
|
+
field: 'function_name',
|
|
32
|
+
matchType: 'not_equal',
|
|
33
|
+
value: 'runtime.usleep',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'stack',
|
|
37
|
+
field: 'function_name',
|
|
38
|
+
matchType: 'not_equal',
|
|
39
|
+
value: 'runtime.futex',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: 'rust_runtime_expected_off_cpu',
|
|
45
|
+
name: 'Rust Expected Off-CPU',
|
|
46
|
+
description: 'Excludes expected Rust runtime blocking functions',
|
|
47
|
+
filters: [
|
|
48
|
+
{
|
|
49
|
+
type: 'stack',
|
|
50
|
+
field: 'function_name',
|
|
51
|
+
matchType: 'not_equal',
|
|
52
|
+
value: 'parking_lot_core::thread_parker::imp::ThreadParker::futex_wait',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: 'stack',
|
|
56
|
+
field: 'function_name',
|
|
57
|
+
matchType: 'not_equal',
|
|
58
|
+
value: 'tokio::runtime::time::Driver::park_internal',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: 'stack',
|
|
62
|
+
field: 'function_name',
|
|
63
|
+
matchType: 'not_equal',
|
|
64
|
+
value: 'futex_wait',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
export const isPresetKey = (key: string): boolean => {
|
|
71
|
+
return filterPresets.some(preset => preset.key === key);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const getPresetByKey = (key: string): FilterPreset | undefined => {
|
|
75
|
+
return filterPresets.find(preset => preset.key === key);
|
|
76
|
+
};
|
|
@@ -18,9 +18,15 @@ import cx from 'classnames';
|
|
|
18
18
|
|
|
19
19
|
import {Button, Input, Select, type SelectItem} from '@parca/components';
|
|
20
20
|
|
|
21
|
+
import {filterPresets, getPresetByKey, isPresetKey} from './filterPresets';
|
|
21
22
|
import {useProfileFilters, type ProfileFilter} from './useProfileFilters';
|
|
22
23
|
|
|
23
24
|
export const isFilterComplete = (filter: ProfileFilter): boolean => {
|
|
25
|
+
// For preset filters, only need type and value
|
|
26
|
+
if (filter.type != null && isPresetKey(filter.type)) {
|
|
27
|
+
return filter.value !== '' && filter.type != null;
|
|
28
|
+
}
|
|
29
|
+
// For regular filters, need all fields
|
|
24
30
|
return (
|
|
25
31
|
filter.value !== '' && filter.type != null && filter.field != null && filter.matchType != null
|
|
26
32
|
);
|
|
@@ -53,6 +59,19 @@ const filterTypeItems: SelectItem[] = [
|
|
|
53
59
|
),
|
|
54
60
|
},
|
|
55
61
|
},
|
|
62
|
+
...filterPresets.map(preset => ({
|
|
63
|
+
key: preset.key,
|
|
64
|
+
element: {
|
|
65
|
+
active: <>{preset.name}</>,
|
|
66
|
+
expanded: (
|
|
67
|
+
<>
|
|
68
|
+
<span>{preset.name}</span>
|
|
69
|
+
<br />
|
|
70
|
+
<span className="text-xs">{preset.description}</span>
|
|
71
|
+
</>
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
})),
|
|
56
75
|
];
|
|
57
76
|
|
|
58
77
|
const fieldItems: SelectItem[] = [
|
|
@@ -181,6 +200,7 @@ const ProfileFilters = (): JSX.Element => {
|
|
|
181
200
|
{filtersToRender.map(filter => {
|
|
182
201
|
const isNumberField = filter.field === 'address' || filter.field === 'line_number';
|
|
183
202
|
const matchTypeItems = isNumberField ? numberMatchTypeItems : stringMatchTypeItems;
|
|
203
|
+
const isPresetFilter = filter.type != null && isPresetKey(filter.type);
|
|
184
204
|
|
|
185
205
|
return (
|
|
186
206
|
<div key={filter.id} className="flex items-center gap-0">
|
|
@@ -189,20 +209,45 @@ const ProfileFilters = (): JSX.Element => {
|
|
|
189
209
|
selectedKey={filter.type}
|
|
190
210
|
placeholder="Select Filter"
|
|
191
211
|
onSelection={key => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
// Check if this is a preset selection
|
|
213
|
+
if (isPresetKey(key)) {
|
|
214
|
+
const preset = getPresetByKey(key);
|
|
215
|
+
if (preset != null) {
|
|
216
|
+
updateFilter(filter.id, {
|
|
217
|
+
type: preset.key,
|
|
218
|
+
field: undefined,
|
|
219
|
+
matchType: undefined,
|
|
220
|
+
value: preset.name,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
const newType = key as 'stack' | 'frame';
|
|
225
|
+
|
|
226
|
+
// Check if we're converting a preset filter to a regular filter
|
|
227
|
+
if (filter.type != null && isPresetKey(filter.type)) {
|
|
228
|
+
updateFilter(filter.id, {
|
|
229
|
+
type: newType,
|
|
230
|
+
field: 'function_name',
|
|
231
|
+
matchType: 'contains',
|
|
232
|
+
value: '',
|
|
233
|
+
});
|
|
234
|
+
} else {
|
|
235
|
+
updateFilter(filter.id, {
|
|
236
|
+
type: newType,
|
|
237
|
+
field: filter.field ?? 'function_name',
|
|
238
|
+
matchType: filter.matchType ?? 'contains',
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
198
242
|
}}
|
|
199
243
|
className={cx(
|
|
200
|
-
'rounded-l-md pr-1 gap-0 focus:z-50 focus:relative focus:outline-1
|
|
201
|
-
|
|
244
|
+
'rounded-l-md pr-1 gap-0 focus:z-50 focus:relative focus:outline-1',
|
|
245
|
+
isPresetFilter ? 'rounded-r-none border-r-0' : 'rounded-r-none',
|
|
246
|
+
filter.type != null ? 'border-r-0 w-auto' : 'w-32'
|
|
202
247
|
)}
|
|
203
248
|
/>
|
|
204
249
|
|
|
205
|
-
{filter.type != null && (
|
|
250
|
+
{filter.type != null && !isPresetFilter && (
|
|
206
251
|
<>
|
|
207
252
|
<Select
|
|
208
253
|
items={fieldItems}
|
|
@@ -247,9 +292,16 @@ const ProfileFilters = (): JSX.Element => {
|
|
|
247
292
|
<Button
|
|
248
293
|
variant="neutral"
|
|
249
294
|
onClick={() => {
|
|
250
|
-
|
|
295
|
+
// If we're displaying local filters and this is the last one, reset everything
|
|
296
|
+
if (localFilters.length > 0 && localFilters.length === 1) {
|
|
251
297
|
resetFilters();
|
|
252
|
-
}
|
|
298
|
+
}
|
|
299
|
+
// If we're displaying applied filters and this is the last one, reset everything
|
|
300
|
+
else if (localFilters.length === 0 && filtersToRender.length === 1) {
|
|
301
|
+
resetFilters();
|
|
302
|
+
}
|
|
303
|
+
// Otherwise, just remove this specific filter
|
|
304
|
+
else {
|
|
253
305
|
removeFilter(filter.id);
|
|
254
306
|
}
|
|
255
307
|
}}
|
|
@@ -278,10 +330,11 @@ const ProfileFilters = (): JSX.Element => {
|
|
|
278
330
|
)}
|
|
279
331
|
</div>
|
|
280
332
|
|
|
281
|
-
{localFilters.length > 0 &&
|
|
333
|
+
{localFilters.length > 0 && (
|
|
282
334
|
<Button
|
|
283
335
|
variant="primary"
|
|
284
336
|
onClick={onApplyFilters}
|
|
337
|
+
disabled={!hasUnsavedChanges || !localFilters.some(isFilterComplete)}
|
|
285
338
|
className={cx('flex items-center gap-2 self-end')}
|
|
286
339
|
>
|
|
287
340
|
<span>Apply</span>
|
|
@@ -22,13 +22,35 @@ import {
|
|
|
22
22
|
type ProfileFilter,
|
|
23
23
|
} from '@parca/store';
|
|
24
24
|
|
|
25
|
+
import {getPresetByKey, isPresetKey, type FilterPreset} from './filterPresets';
|
|
25
26
|
import {useProfileFiltersUrlState} from './useProfileFiltersUrlState';
|
|
26
27
|
|
|
27
28
|
export type {ProfileFilter};
|
|
28
29
|
|
|
29
30
|
// Convert ProfileFilter[] to protobuf Filter[] matching the expected structure
|
|
30
31
|
export const convertToProtoFilters = (profileFilters: ProfileFilter[]): Filter[] => {
|
|
31
|
-
|
|
32
|
+
// First, expand any preset filters to their constituent filters
|
|
33
|
+
const expandedFilters: ProfileFilter[] = [];
|
|
34
|
+
|
|
35
|
+
for (const filter of profileFilters) {
|
|
36
|
+
if (filter.type != null && isPresetKey(filter.type)) {
|
|
37
|
+
// This is a preset filter, expand it
|
|
38
|
+
const preset = getPresetByKey(filter.type);
|
|
39
|
+
if (preset != null) {
|
|
40
|
+
preset.filters.forEach((presetFilter, index) => {
|
|
41
|
+
expandedFilters.push({
|
|
42
|
+
...presetFilter,
|
|
43
|
+
id: `${filter.id}-expanded-${index}`,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
// Regular filter, add as is
|
|
49
|
+
expandedFilters.push(filter);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return expandedFilters
|
|
32
54
|
.filter(f => f.value !== '' && f.type != null && f.field != null && f.matchType != null) // Only include complete filters with values
|
|
33
55
|
.map(f => {
|
|
34
56
|
// Build the condition based on field type
|
|
@@ -120,6 +142,7 @@ export const useProfileFilters = (): {
|
|
|
120
142
|
removeFilter: (id: string) => void;
|
|
121
143
|
updateFilter: (id: string, updates: Partial<ProfileFilter>) => void;
|
|
122
144
|
resetFilters: () => void;
|
|
145
|
+
applyPreset: (preset: FilterPreset) => void;
|
|
123
146
|
} => {
|
|
124
147
|
const {appliedFilters, setAppliedFilters} = useProfileFiltersUrlState();
|
|
125
148
|
const dispatch = useAppDispatch();
|
|
@@ -151,8 +174,23 @@ export const useProfileFilters = (): {
|
|
|
151
174
|
}, []);
|
|
152
175
|
|
|
153
176
|
const hasUnsavedChanges = useMemo(() => {
|
|
154
|
-
const localWithValues = localFilters.filter(f =>
|
|
155
|
-
|
|
177
|
+
const localWithValues = localFilters.filter(f => {
|
|
178
|
+
// For preset filters, only need type and value
|
|
179
|
+
if (f.type != null && isPresetKey(f.type)) {
|
|
180
|
+
return f.value !== '' && f.type != null;
|
|
181
|
+
}
|
|
182
|
+
// For regular filters, need all fields
|
|
183
|
+
return f.value !== '' && f.type != null && f.field != null && f.matchType != null;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const appliedWithValues = (appliedFilters ?? []).filter(f => {
|
|
187
|
+
// For preset filters, only need type and value
|
|
188
|
+
if (f.type != null && isPresetKey(f.type)) {
|
|
189
|
+
return f.value !== '' && f.type != null;
|
|
190
|
+
}
|
|
191
|
+
// For regular filters, need all fields
|
|
192
|
+
return f.value !== '' && f.type != null && f.field != null && f.matchType != null;
|
|
193
|
+
});
|
|
156
194
|
|
|
157
195
|
if (localWithValues.length !== appliedWithValues.length) return true;
|
|
158
196
|
|
|
@@ -252,9 +290,14 @@ export const useProfileFilters = (): {
|
|
|
252
290
|
}, [dispatch, setAppliedFilters]);
|
|
253
291
|
|
|
254
292
|
const onApplyFilters = useCallback((): void => {
|
|
255
|
-
const validFilters = localFilters.filter(
|
|
256
|
-
|
|
257
|
-
|
|
293
|
+
const validFilters = localFilters.filter(f => {
|
|
294
|
+
// For preset filters, only need type and value
|
|
295
|
+
if (f.type != null && isPresetKey(f.type)) {
|
|
296
|
+
return f.value !== '' && f.type != null;
|
|
297
|
+
}
|
|
298
|
+
// For regular filters, need all fields
|
|
299
|
+
return f.value !== '' && f.type != null && f.field != null && f.matchType != null;
|
|
300
|
+
});
|
|
258
301
|
|
|
259
302
|
const filtersToApply = validFilters.map((f, index) => ({
|
|
260
303
|
...f,
|
|
@@ -268,6 +311,23 @@ export const useProfileFilters = (): {
|
|
|
268
311
|
return convertToProtoFilters(appliedFilters ?? []);
|
|
269
312
|
}, [appliedFilters]);
|
|
270
313
|
|
|
314
|
+
const applyPreset = useCallback(
|
|
315
|
+
(preset: FilterPreset) => {
|
|
316
|
+
const presetFilter: ProfileFilter = {
|
|
317
|
+
id: `filter-preset-${Date.now()}`,
|
|
318
|
+
type: preset.key,
|
|
319
|
+
value: preset.name,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Add preset filter to existing filters
|
|
323
|
+
const updatedFilters = [...localFilters, presetFilter];
|
|
324
|
+
dispatch(setLocalFilters(updatedFilters));
|
|
325
|
+
|
|
326
|
+
setAppliedFilters(updatedFilters);
|
|
327
|
+
},
|
|
328
|
+
[dispatch, setAppliedFilters, localFilters]
|
|
329
|
+
);
|
|
330
|
+
|
|
271
331
|
return {
|
|
272
332
|
localFilters,
|
|
273
333
|
appliedFilters,
|
|
@@ -280,5 +340,6 @@ export const useProfileFilters = (): {
|
|
|
280
340
|
removeFilter,
|
|
281
341
|
updateFilter,
|
|
282
342
|
resetFilters,
|
|
343
|
+
applyPreset,
|
|
283
344
|
};
|
|
284
345
|
};
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import {useURLStateCustom, type ParamValueSetterCustom} from '@parca/components';
|
|
15
15
|
import {type ProfileFilter} from '@parca/store';
|
|
16
16
|
|
|
17
|
+
import {isPresetKey} from './filterPresets';
|
|
18
|
+
|
|
17
19
|
// Compact encoding mappings
|
|
18
20
|
const TYPE_MAP: Record<string, string> = {
|
|
19
21
|
stack: 's',
|
|
@@ -46,8 +48,16 @@ const encodeProfileFilters = (filters: ProfileFilter[]): string => {
|
|
|
46
48
|
if (filters.length === 0) return '';
|
|
47
49
|
|
|
48
50
|
return filters
|
|
49
|
-
.filter(f => f.value !== '' && f.type != null
|
|
51
|
+
.filter(f => f.value !== '' && f.type != null)
|
|
50
52
|
.map(f => {
|
|
53
|
+
// Handle preset filters differently
|
|
54
|
+
if (isPresetKey(f.type!)) {
|
|
55
|
+
const presetKey = encodeURIComponent(f.type!);
|
|
56
|
+
const value = encodeURIComponent(f.value);
|
|
57
|
+
return `p:${presetKey}:${value}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle regular filters
|
|
51
61
|
const type = TYPE_MAP[f.type!];
|
|
52
62
|
const field = FIELD_MAP[f.field!];
|
|
53
63
|
const match = MATCH_MAP[f.matchType!];
|
|
@@ -63,7 +73,22 @@ export const decodeProfileFilters = (encoded: string): ProfileFilter[] => {
|
|
|
63
73
|
|
|
64
74
|
try {
|
|
65
75
|
return encoded.split(',').map((filter, index) => {
|
|
66
|
-
const
|
|
76
|
+
const parts = filter.split(':');
|
|
77
|
+
|
|
78
|
+
// Handle preset filters (format: p:presetKey:value)
|
|
79
|
+
if (parts[0] === 'p' && parts.length >= 3) {
|
|
80
|
+
const presetKey = decodeURIComponent(parts[1]);
|
|
81
|
+
const value = decodeURIComponent(parts.slice(2).join(':')); // Handle values with colons
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
id: `filter-${Date.now()}-${index}`,
|
|
85
|
+
type: presetKey,
|
|
86
|
+
value,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle regular filters (format: type:field:match:value)
|
|
91
|
+
const [type, field, match, ...valueParts] = parts;
|
|
67
92
|
const value = decodeURIComponent(valueParts.join(':')); // Handle values with colons
|
|
68
93
|
|
|
69
94
|
return {
|
|
@@ -44,6 +44,7 @@ export const ProfileView = ({
|
|
|
44
44
|
pprofDownloading,
|
|
45
45
|
compare,
|
|
46
46
|
showVisualizationSelector,
|
|
47
|
+
sandwichData,
|
|
47
48
|
}: ProfileViewProps): JSX.Element => {
|
|
48
49
|
const {
|
|
49
50
|
timezone,
|
|
@@ -88,6 +89,7 @@ export const ProfileView = ({
|
|
|
88
89
|
isHalfScreen: boolean;
|
|
89
90
|
}): JSX.Element => {
|
|
90
91
|
return getDashboardItem({
|
|
92
|
+
sandwichData,
|
|
91
93
|
type,
|
|
92
94
|
isHalfScreen,
|
|
93
95
|
dimensions,
|
|
@@ -11,20 +11,12 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
Callgraph as CallgraphType,
|
|
16
|
-
Flamegraph,
|
|
17
|
-
FlamegraphArrow,
|
|
18
|
-
QueryServiceClient,
|
|
19
|
-
Source,
|
|
20
|
-
TableArrow,
|
|
21
|
-
} from '@parca/client';
|
|
14
|
+
import {FlamegraphArrow, QueryServiceClient, Source, TableArrow} from '@parca/client';
|
|
22
15
|
|
|
23
16
|
import {ProfileSource} from '../../ProfileSource';
|
|
24
17
|
|
|
25
18
|
export interface FlamegraphData {
|
|
26
19
|
loading: boolean;
|
|
27
|
-
data?: Flamegraph;
|
|
28
20
|
arrow?: FlamegraphArrow;
|
|
29
21
|
total?: bigint;
|
|
30
22
|
filtered?: bigint;
|
|
@@ -43,20 +35,17 @@ export interface TopTableData {
|
|
|
43
35
|
unit?: string;
|
|
44
36
|
}
|
|
45
37
|
|
|
46
|
-
export interface CallgraphData {
|
|
47
|
-
loading: boolean;
|
|
48
|
-
data?: CallgraphType;
|
|
49
|
-
total?: bigint;
|
|
50
|
-
filtered?: bigint;
|
|
51
|
-
error?: any;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
38
|
export interface SourceData {
|
|
55
39
|
loading: boolean;
|
|
56
40
|
data?: Source;
|
|
57
41
|
error?: any;
|
|
58
42
|
}
|
|
59
43
|
|
|
44
|
+
export interface SandwichData {
|
|
45
|
+
callees: FlamegraphData;
|
|
46
|
+
callers: FlamegraphData;
|
|
47
|
+
}
|
|
48
|
+
|
|
60
49
|
export type VisualizationType =
|
|
61
50
|
| 'flamegraph'
|
|
62
51
|
| 'callgraph'
|
|
@@ -70,8 +59,8 @@ export interface ProfileViewProps {
|
|
|
70
59
|
filtered: bigint;
|
|
71
60
|
flamegraphData: FlamegraphData;
|
|
72
61
|
flamechartData: FlamegraphData;
|
|
62
|
+
sandwichData: SandwichData;
|
|
73
63
|
topTableData?: TopTableData;
|
|
74
|
-
callgraphData?: CallgraphData;
|
|
75
64
|
sourceData?: SourceData;
|
|
76
65
|
profileSource: ProfileSource;
|
|
77
66
|
queryClient?: QueryServiceClient;
|
|
@@ -47,6 +47,7 @@ export const ProfileViewWithData = ({
|
|
|
47
47
|
defaultValue: [FIELD_FUNCTION_NAME],
|
|
48
48
|
alwaysReturnArray: true,
|
|
49
49
|
});
|
|
50
|
+
const [sandwichFunctionName] = useURLState<string | undefined>('sandwich_function_name');
|
|
50
51
|
|
|
51
52
|
const [invertStack] = useURLState('invert_call_stack');
|
|
52
53
|
const invertCallStack = invertStack === 'true';
|
|
@@ -131,15 +132,6 @@ export const ProfileViewWithData = ({
|
|
|
131
132
|
protoFilters,
|
|
132
133
|
});
|
|
133
134
|
|
|
134
|
-
const {
|
|
135
|
-
isLoading: callgraphLoading,
|
|
136
|
-
response: callgraphResponse,
|
|
137
|
-
error: callgraphError,
|
|
138
|
-
} = useQuery(queryClient, profileSource, QueryRequest_ReportType.CALLGRAPH, {
|
|
139
|
-
skip: !dashboardItems.includes('callgraph'),
|
|
140
|
-
protoFilters,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
135
|
const {
|
|
144
136
|
isLoading: sourceLoading,
|
|
145
137
|
response: sourceResponse,
|
|
@@ -151,6 +143,32 @@ export const ProfileViewWithData = ({
|
|
|
151
143
|
protoFilters,
|
|
152
144
|
});
|
|
153
145
|
|
|
146
|
+
const {
|
|
147
|
+
isLoading: callersFlamegraphLoading,
|
|
148
|
+
response: callersFlamegraphResponse,
|
|
149
|
+
error: callersFlamegraphError,
|
|
150
|
+
} = useQuery(queryClient, profileSource, QueryRequest_ReportType.FLAMEGRAPH_ARROW, {
|
|
151
|
+
nodeTrimThreshold,
|
|
152
|
+
groupBy: [FIELD_FUNCTION_NAME],
|
|
153
|
+
invertCallStack: true,
|
|
154
|
+
sandwichByFunction: sandwichFunctionName,
|
|
155
|
+
skip: sandwichFunctionName === undefined && !dashboardItems.includes('sandwich'),
|
|
156
|
+
protoFilters,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const {
|
|
160
|
+
isLoading: calleesFlamegraphLoading,
|
|
161
|
+
response: calleesFlamegraphResponse,
|
|
162
|
+
error: calleesFlamegraphError,
|
|
163
|
+
} = useQuery(queryClient, profileSource, QueryRequest_ReportType.FLAMEGRAPH_ARROW, {
|
|
164
|
+
nodeTrimThreshold,
|
|
165
|
+
groupBy: [FIELD_FUNCTION_NAME],
|
|
166
|
+
invertCallStack: false,
|
|
167
|
+
sandwichByFunction: sandwichFunctionName,
|
|
168
|
+
skip: sandwichFunctionName === undefined && !dashboardItems.includes('sandwich'),
|
|
169
|
+
protoFilters,
|
|
170
|
+
});
|
|
171
|
+
|
|
154
172
|
useEffect(() => {
|
|
155
173
|
if (
|
|
156
174
|
(!flamegraphLoading && flamegraphResponse?.report.oneofKind === 'flamegraph') ||
|
|
@@ -163,18 +181,12 @@ export const ProfileViewWithData = ({
|
|
|
163
181
|
perf?.markInteraction('table render', tableResponse.total);
|
|
164
182
|
}
|
|
165
183
|
|
|
166
|
-
if (!callgraphLoading && callgraphResponse?.report.oneofKind === 'callgraph') {
|
|
167
|
-
perf?.markInteraction('Callgraph render', callgraphResponse.total);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
184
|
if (!sourceLoading && sourceResponse?.report.oneofKind === 'source') {
|
|
171
185
|
perf?.markInteraction('Source render', sourceResponse.total);
|
|
172
186
|
}
|
|
173
187
|
}, [
|
|
174
188
|
flamegraphLoading,
|
|
175
189
|
flamegraphResponse,
|
|
176
|
-
callgraphResponse,
|
|
177
|
-
callgraphLoading,
|
|
178
190
|
tableLoading,
|
|
179
191
|
tableResponse,
|
|
180
192
|
sourceLoading,
|
|
@@ -210,15 +222,18 @@ export const ProfileViewWithData = ({
|
|
|
210
222
|
} else if (tableResponse !== null) {
|
|
211
223
|
total = BigInt(tableResponse.total);
|
|
212
224
|
filtered = BigInt(tableResponse.filtered);
|
|
213
|
-
} else if (callgraphResponse !== null) {
|
|
214
|
-
total = BigInt(callgraphResponse.total);
|
|
215
|
-
filtered = BigInt(callgraphResponse.filtered);
|
|
216
225
|
} else if (sourceResponse !== null) {
|
|
217
226
|
total = BigInt(sourceResponse.total);
|
|
218
227
|
filtered = BigInt(sourceResponse.filtered);
|
|
219
228
|
} else if (flamechartResponse !== null) {
|
|
220
229
|
total = BigInt(flamechartResponse.total);
|
|
221
230
|
filtered = BigInt(flamechartResponse.filtered);
|
|
231
|
+
} else if (callersFlamegraphResponse !== null) {
|
|
232
|
+
total = BigInt(callersFlamegraphResponse.total);
|
|
233
|
+
filtered = BigInt(callersFlamegraphResponse.filtered);
|
|
234
|
+
} else if (calleesFlamegraphResponse !== null) {
|
|
235
|
+
total = BigInt(calleesFlamegraphResponse.total);
|
|
236
|
+
filtered = BigInt(calleesFlamegraphResponse.filtered);
|
|
222
237
|
}
|
|
223
238
|
|
|
224
239
|
return (
|
|
@@ -227,10 +242,6 @@ export const ProfileViewWithData = ({
|
|
|
227
242
|
filtered={filtered}
|
|
228
243
|
flamegraphData={{
|
|
229
244
|
loading: flamegraphLoading && profileMetadataLoading,
|
|
230
|
-
data:
|
|
231
|
-
flamegraphResponse?.report.oneofKind === 'flamegraph'
|
|
232
|
-
? flamegraphResponse?.report?.flamegraph
|
|
233
|
-
: undefined,
|
|
234
245
|
arrow:
|
|
235
246
|
flamegraphResponse?.report.oneofKind === 'flamegraphArrow'
|
|
236
247
|
? flamegraphResponse?.report?.flamegraphArrow
|
|
@@ -279,14 +290,6 @@ export const ProfileViewWithData = ({
|
|
|
279
290
|
? tableResponse.report.tableArrow.unit
|
|
280
291
|
: '',
|
|
281
292
|
}}
|
|
282
|
-
callgraphData={{
|
|
283
|
-
loading: callgraphLoading,
|
|
284
|
-
data:
|
|
285
|
-
callgraphResponse?.report.oneofKind === 'callgraph'
|
|
286
|
-
? callgraphResponse?.report?.callgraph
|
|
287
|
-
: undefined,
|
|
288
|
-
error: callgraphError,
|
|
289
|
-
}}
|
|
290
293
|
sourceData={{
|
|
291
294
|
loading: sourceLoading,
|
|
292
295
|
data:
|
|
@@ -295,6 +298,38 @@ export const ProfileViewWithData = ({
|
|
|
295
298
|
: undefined,
|
|
296
299
|
error: sourceError,
|
|
297
300
|
}}
|
|
301
|
+
sandwichData={{
|
|
302
|
+
callees: {
|
|
303
|
+
arrow:
|
|
304
|
+
calleesFlamegraphResponse?.report.oneofKind === 'flamegraphArrow'
|
|
305
|
+
? calleesFlamegraphResponse?.report?.flamegraphArrow
|
|
306
|
+
: undefined,
|
|
307
|
+
loading: calleesFlamegraphLoading,
|
|
308
|
+
error: calleesFlamegraphError,
|
|
309
|
+
total: BigInt(calleesFlamegraphResponse?.total ?? '0'),
|
|
310
|
+
filtered: BigInt(calleesFlamegraphResponse?.filtered ?? '0'),
|
|
311
|
+
metadataMappingFiles:
|
|
312
|
+
profileMetadataResponse?.report.oneofKind === 'profileMetadata'
|
|
313
|
+
? profileMetadataResponse?.report?.profileMetadata?.mappingFiles
|
|
314
|
+
: undefined,
|
|
315
|
+
metadataLoading: profileMetadataLoading,
|
|
316
|
+
},
|
|
317
|
+
callers: {
|
|
318
|
+
arrow:
|
|
319
|
+
callersFlamegraphResponse?.report.oneofKind === 'flamegraphArrow'
|
|
320
|
+
? callersFlamegraphResponse?.report?.flamegraphArrow
|
|
321
|
+
: undefined,
|
|
322
|
+
loading: callersFlamegraphLoading,
|
|
323
|
+
error: callersFlamegraphError,
|
|
324
|
+
total: BigInt(callersFlamegraphResponse?.total ?? '0'),
|
|
325
|
+
filtered: BigInt(callersFlamegraphResponse?.filtered ?? '0'),
|
|
326
|
+
metadataMappingFiles:
|
|
327
|
+
profileMetadataResponse?.report.oneofKind === 'profileMetadata'
|
|
328
|
+
? profileMetadataResponse?.report?.profileMetadata?.mappingFiles
|
|
329
|
+
: undefined,
|
|
330
|
+
metadataLoading: profileMetadataLoading,
|
|
331
|
+
},
|
|
332
|
+
}}
|
|
298
333
|
profileSource={profileSource}
|
|
299
334
|
queryClient={queryClient}
|
|
300
335
|
onDownloadPProf={() => void downloadPProfClick()}
|