@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/ProfileView/components/ColorStackLegend.d.ts.map +1 -1
  3. package/dist/ProfileView/components/ColorStackLegend.js +0 -1
  4. package/dist/ProfileView/components/DashboardItems/index.d.ts +3 -2
  5. package/dist/ProfileView/components/DashboardItems/index.d.ts.map +1 -1
  6. package/dist/ProfileView/components/DashboardItems/index.js +2 -2
  7. package/dist/ProfileView/components/ProfileFilters/filterPresets.d.ts +11 -0
  8. package/dist/ProfileView/components/ProfileFilters/filterPresets.d.ts.map +1 -0
  9. package/dist/ProfileView/components/ProfileFilters/filterPresets.js +64 -0
  10. package/dist/ProfileView/components/ProfileFilters/index.d.ts.map +1 -1
  11. package/dist/ProfileView/components/ProfileFilters/index.js +54 -9
  12. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts +2 -0
  13. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts.map +1 -1
  14. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.js +58 -4
  15. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
  16. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +22 -2
  17. package/dist/ProfileView/index.d.ts +1 -1
  18. package/dist/ProfileView/index.d.ts.map +1 -1
  19. package/dist/ProfileView/index.js +2 -1
  20. package/dist/ProfileView/types/visualization.d.ts +6 -10
  21. package/dist/ProfileView/types/visualization.d.ts.map +1 -1
  22. package/dist/ProfileViewWithData.d.ts.map +1 -1
  23. package/dist/ProfileViewWithData.js +52 -22
  24. package/dist/Sandwich/components/CalleesSection.d.ts +3 -12
  25. package/dist/Sandwich/components/CalleesSection.d.ts.map +1 -1
  26. package/dist/Sandwich/components/CalleesSection.js +2 -4
  27. package/dist/Sandwich/components/CallersSection.d.ts +3 -13
  28. package/dist/Sandwich/components/CallersSection.d.ts.map +1 -1
  29. package/dist/Sandwich/components/CallersSection.js +5 -8
  30. package/dist/Sandwich/index.d.ts +2 -10
  31. package/dist/Sandwich/index.d.ts.map +1 -1
  32. package/dist/Sandwich/index.js +5 -103
  33. package/dist/styles.css +1 -1
  34. package/package.json +6 -6
  35. package/src/ProfileView/components/ColorStackLegend.tsx +0 -2
  36. package/src/ProfileView/components/DashboardItems/index.tsx +4 -12
  37. package/src/ProfileView/components/ProfileFilters/filterPresets.ts +76 -0
  38. package/src/ProfileView/components/ProfileFilters/index.tsx +65 -12
  39. package/src/ProfileView/components/ProfileFilters/useProfileFilters.ts +67 -6
  40. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +27 -2
  41. package/src/ProfileView/index.tsx +2 -0
  42. package/src/ProfileView/types/visualization.ts +7 -18
  43. package/src/ProfileViewWithData.tsx +65 -30
  44. package/src/Sandwich/components/CalleesSection.tsx +10 -28
  45. package/src/Sandwich/components/CallersSection.tsx +13 -34
  46. 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
- const newType = key as 'stack' | 'frame';
193
- updateFilter(filter.id, {
194
- type: newType,
195
- field: filter.field ?? 'function_name',
196
- matchType: filter.matchType ?? 'contains',
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 rounded-r-none ',
201
- filter.type != null ? 'border-r-0 w-28' : 'w-32'
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
- if (localFilters.length === 1) {
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
- } else {
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 && hasUnsavedChanges && localFilters.some(isFilterComplete) && (
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
- return profileFilters
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 => f.value !== '');
155
- const appliedWithValues = (appliedFilters ?? []).filter(f => f.value !== '');
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
- f => f.value !== '' && f.type != null && f.field != null && f.matchType != null
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 && f.field != null && f.matchType != 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 [type, field, match, ...valueParts] = filter.split(':');
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()}