@parca/profile 0.19.113 → 0.19.114

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 (86) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts.map +1 -1
  3. package/dist/ProfileExplorer/ProfileExplorerSingle.js +9 -3
  4. package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.d.ts +31 -0
  5. package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.d.ts.map +1 -0
  6. package/dist/{MetricsGraphStrips/AreaGraph → ProfileFlameChart/SamplesStrips/SamplesGraph}/index.js +32 -60
  7. package/dist/{MetricsGraphStrips/MetricsGraphStrips.stories.d.ts → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.d.ts} +4 -3
  8. package/dist/ProfileFlameChart/SamplesStrips/SamplesStrips.stories.d.ts.map +1 -0
  9. package/dist/{MetricsGraphStrips/MetricsGraphStrips.stories.js → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.js} +5 -4
  10. package/dist/{MetricsGraphStrips → ProfileFlameChart/SamplesStrips}/index.d.ts +5 -4
  11. package/dist/ProfileFlameChart/SamplesStrips/index.d.ts.map +1 -0
  12. package/dist/ProfileFlameChart/SamplesStrips/index.js +141 -0
  13. package/dist/ProfileFlameChart/index.d.ts +20 -0
  14. package/dist/ProfileFlameChart/index.d.ts.map +1 -0
  15. package/dist/ProfileFlameChart/index.js +155 -0
  16. package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
  17. package/dist/ProfileFlameGraph/index.js +0 -1
  18. package/dist/ProfileMetricsGraph/hooks/useQueryRange.d.ts +2 -1
  19. package/dist/ProfileMetricsGraph/hooks/useQueryRange.d.ts.map +1 -1
  20. package/dist/ProfileMetricsGraph/hooks/useQueryRange.js +11 -21
  21. package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
  22. package/dist/ProfileMetricsGraph/index.js +13 -3
  23. package/dist/ProfileSelector/index.d.ts.map +1 -1
  24. package/dist/ProfileSelector/index.js +4 -0
  25. package/dist/ProfileView/components/ActionButtons/GroupByDropdown.d.ts +1 -0
  26. package/dist/ProfileView/components/ActionButtons/GroupByDropdown.d.ts.map +1 -1
  27. package/dist/ProfileView/components/ActionButtons/GroupByDropdown.js +2 -2
  28. package/dist/ProfileView/components/DashboardItems/index.d.ts +5 -4
  29. package/dist/ProfileView/components/DashboardItems/index.d.ts.map +1 -1
  30. package/dist/ProfileView/components/DashboardItems/index.js +4 -3
  31. package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts +2 -1
  32. package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts.map +1 -1
  33. package/dist/ProfileView/components/GroupByLabelsDropdown/index.js +2 -2
  34. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +1 -1
  35. package/dist/ProfileView/components/Toolbars/index.d.ts +2 -0
  36. package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
  37. package/dist/ProfileView/components/Toolbars/index.js +4 -2
  38. package/dist/ProfileView/hooks/useAutoSelectDimension.d.ts +16 -0
  39. package/dist/ProfileView/hooks/useAutoSelectDimension.d.ts.map +1 -0
  40. package/dist/ProfileView/hooks/useAutoSelectDimension.js +75 -0
  41. package/dist/ProfileView/hooks/useVisualizationState.d.ts +2 -0
  42. package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
  43. package/dist/ProfileView/hooks/useVisualizationState.js +8 -0
  44. package/dist/ProfileView/index.d.ts +1 -1
  45. package/dist/ProfileView/index.d.ts.map +1 -1
  46. package/dist/ProfileView/index.js +7 -4
  47. package/dist/ProfileView/types/visualization.d.ts +15 -3
  48. package/dist/ProfileView/types/visualization.d.ts.map +1 -1
  49. package/dist/ProfileViewWithData.d.ts +2 -1
  50. package/dist/ProfileViewWithData.d.ts.map +1 -1
  51. package/dist/ProfileViewWithData.js +41 -29
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +4 -0
  55. package/dist/styles.css +1 -1
  56. package/package.json +8 -7
  57. package/src/ProfileExplorer/ProfileExplorerSingle.tsx +14 -3
  58. package/src/{MetricsGraphStrips/AreaGraph → ProfileFlameChart/SamplesStrips/SamplesGraph}/index.tsx +77 -81
  59. package/src/{MetricsGraphStrips/MetricsGraphStrips.stories.tsx → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.tsx} +7 -6
  60. package/src/ProfileFlameChart/SamplesStrips/index.tsx +301 -0
  61. package/src/ProfileFlameChart/index.tsx +305 -0
  62. package/src/ProfileFlameGraph/index.tsx +0 -1
  63. package/src/ProfileMetricsGraph/hooks/useQueryRange.ts +18 -26
  64. package/src/ProfileMetricsGraph/index.tsx +24 -2
  65. package/src/ProfileSelector/index.tsx +11 -0
  66. package/src/ProfileView/components/ActionButtons/GroupByDropdown.tsx +3 -0
  67. package/src/ProfileView/components/DashboardItems/index.tsx +19 -17
  68. package/src/ProfileView/components/GroupByLabelsDropdown/index.tsx +4 -2
  69. package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +1 -1
  70. package/src/ProfileView/components/Toolbars/index.tsx +18 -1
  71. package/src/ProfileView/hooks/useAutoSelectDimension.ts +90 -0
  72. package/src/ProfileView/hooks/useVisualizationState.ts +17 -0
  73. package/src/ProfileView/index.tsx +16 -2
  74. package/src/ProfileView/types/visualization.ts +17 -3
  75. package/src/ProfileViewWithData.tsx +80 -37
  76. package/src/index.tsx +4 -0
  77. package/dist/MetricsGraphStrips/AreaGraph/Tooltip.d.ts +0 -10
  78. package/dist/MetricsGraphStrips/AreaGraph/Tooltip.d.ts.map +0 -1
  79. package/dist/MetricsGraphStrips/AreaGraph/Tooltip.js +0 -44
  80. package/dist/MetricsGraphStrips/AreaGraph/index.d.ts +0 -21
  81. package/dist/MetricsGraphStrips/AreaGraph/index.d.ts.map +0 -1
  82. package/dist/MetricsGraphStrips/MetricsGraphStrips.stories.d.ts.map +0 -1
  83. package/dist/MetricsGraphStrips/index.d.ts.map +0 -1
  84. package/dist/MetricsGraphStrips/index.js +0 -70
  85. package/src/MetricsGraphStrips/AreaGraph/Tooltip.tsx +0 -83
  86. package/src/MetricsGraphStrips/index.tsx +0 -142
@@ -11,20 +11,24 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {useEffect, useMemo} from 'react';
15
-
16
14
  import {RpcError} from '@protobuf-ts/runtime-rpc';
17
15
 
18
16
  import {Duration, QueryRangeResponse, QueryServiceClient, Timestamp} from '@parca/client';
19
- import {useGrpcMetadata, useURLState} from '@parca/components';
20
- import {getStepDuration} from '@parca/utilities';
17
+ import {useGrpcMetadata} from '@parca/components';
18
+ import {getStepDuration, getStepDurationInMilliseconds} from '@parca/utilities';
21
19
 
22
20
  import useGrpcQuery from '../../useGrpcQuery';
23
21
 
22
+ interface QueryRangeResult {
23
+ response: QueryRangeResponse;
24
+ stepDurationMs: number;
25
+ }
26
+
24
27
  interface IQueryRangeState {
25
28
  response: QueryRangeResponse | null;
26
29
  isLoading: boolean;
27
30
  error: RpcError | null;
31
+ stepDurationMs: number;
28
32
  }
29
33
 
30
34
  export const getStepCountFromScreenWidth = (pixelsPerPoint: number): number => {
@@ -43,33 +47,16 @@ export const useQueryRange = (
43
47
  start: number,
44
48
  end: number,
45
49
  sumBy: string[],
50
+ stepCount: number,
46
51
  skip = false
47
52
  ): IQueryRangeState => {
48
53
  const metadata = useGrpcMetadata();
49
- const [stepCountStr, setStepCount] = useURLState('step_count');
50
-
51
- const defaultStepCount = useMemo(() => {
52
- return getStepCountFromScreenWidth(10);
53
- }, []);
54
-
55
- const stepCount = useMemo(() => {
56
- if (stepCountStr != null) {
57
- return parseInt(stepCountStr as string, 10);
58
- }
59
-
60
- return defaultStepCount;
61
- }, [stepCountStr, defaultStepCount]);
62
-
63
- useEffect(() => {
64
- if (stepCountStr == null) {
65
- setStepCount(defaultStepCount.toString());
66
- }
67
- }, [stepCountStr, defaultStepCount, setStepCount]);
68
54
 
69
- const {data, isLoading, error} = useGrpcQuery<QueryRangeResponse | undefined>({
55
+ const {data, isLoading, error} = useGrpcQuery<QueryRangeResult | undefined>({
70
56
  key: ['query-range', queryExpression, start, end, (sumBy ?? []).join(','), stepCount, metadata],
71
57
  queryFn: async signal => {
72
58
  const stepDuration = getStepDuration(start, end, stepCount);
59
+ const stepDurationMs = getStepDurationInMilliseconds(stepDuration);
73
60
  const {response} = await client.queryRange(
74
61
  {
75
62
  query: queryExpression,
@@ -81,7 +68,7 @@ export const useQueryRange = (
81
68
  },
82
69
  {meta: metadata, abort: signal}
83
70
  );
84
- return response;
71
+ return {response, stepDurationMs};
85
72
  },
86
73
  options: {
87
74
  retry: false,
@@ -90,5 +77,10 @@ export const useQueryRange = (
90
77
  },
91
78
  });
92
79
 
93
- return {isLoading, error: error as RpcError | null, response: data ?? null};
80
+ return {
81
+ isLoading,
82
+ error: error as RpcError | null,
83
+ response: data?.response ?? null,
84
+ stepDurationMs: data?.stepDurationMs ?? 0,
85
+ };
94
86
  };
@@ -25,8 +25,11 @@ import {
25
25
  import {
26
26
  DateTimeRange,
27
27
  MetricsGraphSkeleton,
28
+ NumberParser,
29
+ NumberSerializer,
28
30
  TextWithTooltip,
29
31
  useParcaContext,
32
+ useURLStateCustom,
30
33
  } from '@parca/components';
31
34
  import {Query} from '@parca/parser';
32
35
  import {TEST_IDS, testId} from '@parca/test-utils';
@@ -35,7 +38,7 @@ import {capitalizeOnlyFirstLetter, formatDate, timePattern, valueFormatter} from
35
38
  import {MergedProfileSelection, ProfileSelection} from '..';
36
39
  import MetricsGraph, {ContextMenuItemOrSubmenu, Series, SeriesPoint} from '../MetricsGraph';
37
40
  import {useMetricsGraphDimensions} from '../MetricsGraph/useMetricsGraphDimensions';
38
- import {useQueryRange} from './hooks/useQueryRange';
41
+ import {getStepCountFromScreenWidth, useQueryRange} from './hooks/useQueryRange';
39
42
 
40
43
  const createProfileContextMenuItems = (
41
44
  addLabelMatcher: (
@@ -197,11 +200,30 @@ const ProfileMetricsGraph = ({
197
200
  comparing = false,
198
201
  sumBy,
199
202
  }: ProfileMetricsGraphProps): JSX.Element => {
203
+ const [rawStepCount] = useURLStateCustom<number>('step_count', {
204
+ defaultValue: String(getStepCountFromScreenWidth(10)),
205
+ parse: NumberParser,
206
+ stringify: NumberSerializer,
207
+ });
208
+ // Clamp step count so the step duration is at least 1 second as we don't have this enforced server-side anymore.
209
+ const stepCount = useMemo(() => {
210
+ const maxForOneSecond = Math.floor((to - from) / 1000);
211
+ return Math.min(rawStepCount, maxForOneSecond);
212
+ }, [rawStepCount, from, to]);
213
+
200
214
  const {
201
215
  isLoading: metricsGraphLoading,
202
216
  response,
203
217
  error,
204
- } = useQueryRange(queryClient, queryExpression, from, to, sumBy, queryExpression === '');
218
+ } = useQueryRange(
219
+ queryClient,
220
+ queryExpression,
221
+ from,
222
+ to,
223
+ sumBy,
224
+ stepCount,
225
+ queryExpression === ''
226
+ );
205
227
  const {onError, perf, authenticationErrorMessage, isDarkMode, timezone} = useParcaContext();
206
228
  const {width, height, margin, heightStyle} = useMetricsGraphDimensions(comparing);
207
229
  const [showAllSeriesForResponse, setShowAllSeriesForResponse] = useState<typeof response | null>(
@@ -153,6 +153,17 @@ const ProfileSelector = ({
153
153
  DateTimeRange.fromRangeKey(draftSelection.timeSelection, draftSelection.from, draftSelection.to)
154
154
  );
155
155
 
156
+ // Sync local timeRangeSelection when URL state changes externally (e.g., "Switch to 1 minute" button)
157
+ useEffect(() => {
158
+ setTimeRangeSelection(
159
+ DateTimeRange.fromRangeKey(
160
+ querySelection.timeSelection,
161
+ querySelection.from,
162
+ querySelection.to
163
+ )
164
+ );
165
+ }, [querySelection.timeSelection, querySelection.from, querySelection.to]);
166
+
156
167
  const [queryExpressionString, setQueryExpressionString] = useState(draftSelection.expression);
157
168
 
158
169
  const [advancedModeForQueryBrowser, setAdvancedModeForQueryBrowser] = useState(
@@ -21,6 +21,7 @@ interface GroupByControlsProps {
21
21
  setGroupByLabels: (labels: string[]) => void;
22
22
  metadataRefetch?: () => Promise<void>;
23
23
  metadataLoading: boolean;
24
+ label?: string;
24
25
  }
25
26
 
26
27
  const GroupByControls: React.FC<GroupByControlsProps> = ({
@@ -29,6 +30,7 @@ const GroupByControls: React.FC<GroupByControlsProps> = ({
29
30
  setGroupByLabels,
30
31
  metadataRefetch,
31
32
  metadataLoading,
33
+ label,
32
34
  }) => {
33
35
  return (
34
36
  <div className="relative flex" id="h-group-by-controls">
@@ -38,6 +40,7 @@ const GroupByControls: React.FC<GroupByControlsProps> = ({
38
40
  setGroupByLabels={setGroupByLabels}
39
41
  metadataRefetch={metadataRefetch}
40
42
  metadataLoading={metadataLoading}
43
+ label={label}
41
44
  />
42
45
  </div>
43
46
  );
@@ -16,6 +16,7 @@ import {Profiler, ProfilerOnRenderCallback} from 'react';
16
16
  import {QueryServiceClient} from '@parca/client';
17
17
  import {ConditionalWrapper} from '@parca/components';
18
18
 
19
+ import ProfileFlameChart from '../../../ProfileFlameChart';
19
20
  import ProfileFlameGraph from '../../../ProfileFlameGraph';
20
21
  import {CurrentPathFrame} from '../../../ProfileFlameGraph/FlameGraphArrow/utils';
21
22
  import {ProfileSource} from '../../../ProfileSource';
@@ -24,6 +25,7 @@ import {SourceView} from '../../../SourceView';
24
25
  import {Table} from '../../../Table';
25
26
  import type {
26
27
  FlamegraphData,
28
+ SamplesData,
27
29
  SandwichData,
28
30
  SourceData,
29
31
  TopTableData,
@@ -35,7 +37,7 @@ interface GetDashboardItemProps {
35
37
  isHalfScreen: boolean;
36
38
  dimensions: DOMRect | undefined;
37
39
  flamegraphData: FlamegraphData;
38
- flamechartData: FlamegraphData;
40
+ samplesData?: SamplesData;
39
41
  topTableData?: TopTableData;
40
42
  sandwichData: SandwichData;
41
43
  sourceData?: SourceData;
@@ -47,7 +49,8 @@ interface GetDashboardItemProps {
47
49
  perf?: {
48
50
  onRender?: ProfilerOnRenderCallback;
49
51
  };
50
- queryClient?: QueryServiceClient;
52
+ queryClient: QueryServiceClient;
53
+ onSwitchToOneMinute?: () => void;
51
54
  }
52
55
 
53
56
  export const getDashboardItem = ({
@@ -55,7 +58,7 @@ export const getDashboardItem = ({
55
58
  isHalfScreen,
56
59
  dimensions,
57
60
  flamegraphData,
58
- flamechartData,
61
+ samplesData,
59
62
  topTableData,
60
63
  sourceData,
61
64
  sandwichData,
@@ -65,6 +68,8 @@ export const getDashboardItem = ({
65
68
  curPathArrow,
66
69
  setNewCurPathArrow,
67
70
  perf,
71
+ queryClient,
72
+ onSwitchToOneMinute,
68
73
  }: GetDashboardItemProps): JSX.Element => {
69
74
  switch (type) {
70
75
  case 'flamegraph':
@@ -102,16 +107,10 @@ export const getDashboardItem = ({
102
107
  );
103
108
  case 'flamechart':
104
109
  return (
105
- <ProfileFlameGraph
106
- curPathArrow={[]}
107
- setNewCurPathArrow={() => {}}
108
- arrow={flamechartData?.arrow}
109
- total={total}
110
- filtered={filtered}
111
- profileType={profileSource?.ProfileType()}
112
- loading={flamechartData.loading}
113
- error={flamechartData.error}
114
- isHalfScreen={isHalfScreen}
110
+ <ProfileFlameChart
111
+ samplesData={samplesData}
112
+ queryClient={queryClient}
113
+ profileSource={profileSource}
115
114
  width={
116
115
  dimensions?.width !== undefined
117
116
  ? isHalfScreen
@@ -119,10 +118,13 @@ export const getDashboardItem = ({
119
118
  : dimensions.width - 16
120
119
  : 0
121
120
  }
122
- metadataMappingFiles={flamechartData.metadataMappingFiles}
123
- metadataLoading={flamechartData.metadataLoading}
124
- profileSource={profileSource}
125
- isFlameChart={true}
121
+ total={total}
122
+ filtered={filtered}
123
+ profileType={profileSource?.ProfileType()}
124
+ isHalfScreen={isHalfScreen}
125
+ metadataMappingFiles={flamegraphData.metadataMappingFiles}
126
+ metadataLoading={flamegraphData.metadataLoading}
127
+ onSwitchToOneMinute={onSwitchToOneMinute}
126
128
  />
127
129
  );
128
130
  case 'table':
@@ -27,6 +27,7 @@ interface Props {
27
27
  setGroupByLabels: (labels: string[]) => void;
28
28
  metadataRefetch?: () => Promise<void>;
29
29
  metadataLoading: boolean;
30
+ label?: string;
30
31
  }
31
32
 
32
33
  const GroupByLabelsDropdown = ({
@@ -35,12 +36,13 @@ const GroupByLabelsDropdown = ({
35
36
  setGroupByLabels,
36
37
  metadataRefetch,
37
38
  metadataLoading,
39
+ label = 'Group by',
38
40
  }: Props): JSX.Element => {
39
41
  return (
40
42
  <div className="flex flex-col relative" {...testId(TEST_IDS.GROUP_BY_CONTAINER)}>
41
43
  <div className="flex items-center justify-between">
42
44
  <label className="text-sm" {...testId(TEST_IDS.GROUP_BY_LABEL)}>
43
- Group by
45
+ {label}
44
46
  </label>
45
47
  </div>
46
48
 
@@ -56,7 +58,7 @@ const GroupByLabelsDropdown = ({
56
58
  refreshTitle="Refresh label names"
57
59
  refreshTestId="group-by-refresh-button"
58
60
  menuTestId={TEST_IDS.GROUP_BY_SELECT_FLYOUT}
59
- value={groupBy
61
+ value={(groupBy ?? [])
60
62
  .filter(l => l.startsWith(FIELD_LABELS))
61
63
  .map(l => ({value: l, label: l.slice(FIELD_LABELS.length + 1)}))}
62
64
  onChange={newValue => {
@@ -412,7 +412,7 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
412
412
  <Menu.Items
413
413
  className={cx(
414
414
  isTableVizOnly ? 'w-64' : 'w-80',
415
- 'absolute z-30 mt-2 py-2 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none border dark:bg-gray-900 dark:border-gray-600',
415
+ 'absolute z-50 mt-2 py-2 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none border dark:bg-gray-900 dark:border-gray-600',
416
416
  shouldOpenLeft ? 'right-0 origin-top-right' : 'left-0 origin-top-left'
417
417
  )}
418
418
  >
@@ -47,6 +47,8 @@ export interface VisualisationToolbarProps {
47
47
  preferencesModal?: boolean;
48
48
  profileViewExternalSubActions?: React.ReactNode;
49
49
  setGroupByLabels: (labels: string[]) => void;
50
+ flamechartDimension: string[];
51
+ setFlamechartDimension: (labels: string[]) => void;
50
52
  showVisualizationSelector?: boolean;
51
53
  sandwichFunctionName?: string;
52
54
  alignFunctionName: string;
@@ -135,6 +137,8 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
135
137
  groupBy,
136
138
  toggleGroupBy,
137
139
  setGroupByLabels,
140
+ flamechartDimension,
141
+ setFlamechartDimension,
138
142
  profileType,
139
143
  profileSource,
140
144
  queryClient,
@@ -158,6 +162,8 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
158
162
  const isTableVizOnly = dashboardItems?.length === 1 && isTableViz;
159
163
  const isGraphViz = dashboardItems?.includes('flamegraph');
160
164
  const isGraphVizOnly = dashboardItems?.length === 1 && isGraphViz;
165
+ const isFlamechartViz = dashboardItems?.includes('flamechart');
166
+ const isFlamechartVizOnly = dashboardItems?.length === 1 && isFlamechartViz;
161
167
 
162
168
  const req = profileSource?.QueryRequest();
163
169
  if (req !== null && req !== undefined) {
@@ -184,8 +190,19 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
184
190
  </>
185
191
  )}
186
192
 
193
+ {isFlamechartViz && (
194
+ <GroupByDropdown
195
+ groupBy={flamechartDimension}
196
+ labels={groupByLabels}
197
+ setGroupByLabels={setFlamechartDimension}
198
+ metadataRefetch={metadataRefetch}
199
+ metadataLoading={metadataLoading}
200
+ label="Samples group by"
201
+ />
202
+ )}
203
+
187
204
  <div className="flex mt-5">
188
- <ProfileFilters />
205
+ {!isFlamechartVizOnly && <ProfileFilters />}
189
206
 
190
207
  {profileViewExternalSubActions != null ? profileViewExternalSubActions : null}
191
208
  </div>
@@ -0,0 +1,90 @@
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 {useEffect, useRef} from 'react';
15
+
16
+ import {ProfileType} from '@parca/parser';
17
+
18
+ import {wellKnownProfiles} from '../../ProfileTypeSelector';
19
+
20
+ const CPU_PREFERRED_DIMENSIONS = ['cpu', 'cpuid', 'thread', 'thread_id'];
21
+
22
+ const GPU_DIMENSIONS = ['node', 'gpu', 'stream'];
23
+
24
+ const getWellKnownProfileName = (profileType?: ProfileType): string | undefined => {
25
+ if (profileType == null) return undefined;
26
+ const key = profileType.toString();
27
+ return wellKnownProfiles[key]?.name;
28
+ };
29
+
30
+ const isOnGpuProfile = (profileType?: ProfileType): boolean => {
31
+ const wellKnownName = getWellKnownProfileName(profileType);
32
+ return wellKnownName === 'On-GPU';
33
+ };
34
+
35
+ const isOnCpuProfile = (profileType?: ProfileType): boolean => {
36
+ const wellKnownName = getWellKnownProfileName(profileType);
37
+ return wellKnownName === 'On-CPU';
38
+ };
39
+
40
+ /**
41
+ * Auto-selects the best flamechart "group by" dimension on first load.
42
+ *
43
+ * For On-GPU profiles:
44
+ * - Selects all available from: ['node', 'gpu', 'stream']
45
+ *
46
+ * For On-CPU profiles:
47
+ * - Priority: cpu > cpuid > thread > thread_id
48
+ * - Adds 'node' alongside the primary dimension if available
49
+ *
50
+ * For all other profile types:
51
+ * - No auto-selection
52
+ */
53
+ export const useAutoSelectDimension = (
54
+ metadataLabels: string[] | undefined,
55
+ flamechartDimension: string[] | undefined,
56
+ setFlamechartDimension: (v: string[]) => void,
57
+ profileType?: ProfileType
58
+ ): void => {
59
+ const hasAutoSelected = useRef(false);
60
+
61
+ useEffect(() => {
62
+ if (hasAutoSelected.current) return;
63
+ if (metadataLabels == null || metadataLabels.length === 0) return;
64
+ if ((flamechartDimension ?? []).length > 0) {
65
+ hasAutoSelected.current = true;
66
+ return;
67
+ }
68
+
69
+ if (isOnGpuProfile(profileType)) {
70
+ const availableGpuDims = GPU_DIMENSIONS.filter(d => metadataLabels.includes(d));
71
+ if (availableGpuDims.length > 0) {
72
+ setFlamechartDimension(availableGpuDims.map(d => `labels.${d}`));
73
+ hasAutoSelected.current = true;
74
+ return;
75
+ }
76
+ }
77
+
78
+ if (isOnCpuProfile(profileType)) {
79
+ const hasNode = metadataLabels.includes('node');
80
+ for (const name of CPU_PREFERRED_DIMENSIONS) {
81
+ if (metadataLabels.includes(name)) {
82
+ const dims = hasNode ? ['node', name] : [name];
83
+ setFlamechartDimension(dims.map(d => `labels.${d}`));
84
+ hasAutoSelected.current = true;
85
+ return;
86
+ }
87
+ }
88
+ }
89
+ }, [metadataLabels, flamechartDimension, setFlamechartDimension, profileType]);
90
+ };
@@ -42,6 +42,8 @@ export const useVisualizationState = (): {
42
42
  setGroupBy: (keys: string[]) => void;
43
43
  toggleGroupBy: (key: string) => void;
44
44
  setGroupByLabels: (labels: string[]) => void;
45
+ flamechartDimension: string[];
46
+ setFlamechartDimension: (labels: string[]) => void;
45
47
  sandwichFunctionName: string | undefined;
46
48
  setSandwichFunctionName: (sandwichFunctionName: string | undefined) => void;
47
49
  resetSandwichFunctionName: () => void;
@@ -74,6 +76,12 @@ export const useVisualizationState = (): {
74
76
  const [sandwichFunctionName, setSandwichFunctionName] = useURLState<string | undefined>(
75
77
  'sandwich_function_name'
76
78
  );
79
+ const [flamechartDimension, setStoreFlamechartDimension] = useURLState<string[]>(
80
+ 'flamechart_dimension',
81
+ {
82
+ alwaysReturnArray: true,
83
+ }
84
+ );
77
85
  const resetFlameGraphState = useResetFlameGraphState();
78
86
  const batchUpdates = useURLStateBatch();
79
87
 
@@ -123,6 +131,13 @@ export const useVisualizationState = (): {
123
131
  [groupBy, setGroupBy, resetFlameGraphState, batchUpdates]
124
132
  );
125
133
 
134
+ const setFlamechartDimension = useCallback(
135
+ (labels: string[]): void => {
136
+ setStoreFlamechartDimension(labels.filter(l => l.startsWith(`${FIELD_LABELS}.`)));
137
+ },
138
+ [setStoreFlamechartDimension]
139
+ );
140
+
126
141
  const resetSandwichFunctionName = useCallback((): void => {
127
142
  setSandwichFunctionName(undefined);
128
143
  }, [setSandwichFunctionName]);
@@ -153,6 +168,8 @@ export const useVisualizationState = (): {
153
168
  setGroupBy,
154
169
  toggleGroupBy,
155
170
  setGroupByLabels,
171
+ flamechartDimension,
172
+ setFlamechartDimension,
156
173
  sandwichFunctionName,
157
174
  setSandwichFunctionName,
158
175
  resetSandwichFunctionName,
@@ -27,6 +27,7 @@ import {
27
27
  } from './components/Toolbars';
28
28
  import {DashboardProvider} from './context/DashboardContext';
29
29
  import {ProfileViewContextProvider} from './context/ProfileViewContext';
30
+ import {useAutoSelectDimension} from './hooks/useAutoSelectDimension';
30
31
  import {useProfileMetadata} from './hooks/useProfileMetadata';
31
32
  import {useVisualizationState} from './hooks/useVisualizationState';
32
33
  import type {ProfileViewProps, VisualizationType} from './types/visualization';
@@ -35,7 +36,7 @@ export const ProfileView = ({
35
36
  total,
36
37
  filtered,
37
38
  flamegraphData,
38
- flamechartData,
39
+ samplesData,
39
40
  topTableData,
40
41
  sourceData,
41
42
  profileSource,
@@ -45,6 +46,7 @@ export const ProfileView = ({
45
46
  compare,
46
47
  showVisualizationSelector,
47
48
  sandwichData,
49
+ onSwitchToOneMinute,
48
50
  }: ProfileViewProps): JSX.Element => {
49
51
  const {
50
52
  timezone,
@@ -64,12 +66,21 @@ export const ProfileView = ({
64
66
  groupBy,
65
67
  toggleGroupBy,
66
68
  setGroupByLabels,
69
+ flamechartDimension,
70
+ setFlamechartDimension,
67
71
  sandwichFunctionName,
68
72
  resetSandwichFunctionName,
69
73
  alignFunctionName,
70
74
  setAlignFunctionName,
71
75
  } = useVisualizationState();
72
76
 
77
+ useAutoSelectDimension(
78
+ flamegraphData.metadataLabels,
79
+ flamechartDimension,
80
+ setFlamechartDimension,
81
+ profileSource?.ProfileType()
82
+ );
83
+
73
84
  const {colorMappings} = useProfileMetadata({
74
85
  flamegraphArrow: flamegraphData.arrow,
75
86
  metadataMappingFiles: flamegraphData.metadataMappingFiles,
@@ -95,7 +106,7 @@ export const ProfileView = ({
95
106
  isHalfScreen,
96
107
  dimensions,
97
108
  flamegraphData,
98
- flamechartData,
109
+ samplesData,
99
110
  topTableData,
100
111
  sourceData,
101
112
  profileSource,
@@ -105,6 +116,7 @@ export const ProfileView = ({
105
116
  setNewCurPathArrow: setCurPathArrow,
106
117
  perf,
107
118
  queryClient,
119
+ onSwitchToOneMinute,
108
120
  });
109
121
  };
110
122
 
@@ -153,6 +165,8 @@ export const ProfileView = ({
153
165
  preferencesModal={preferencesModal}
154
166
  profileViewExternalSubActions={profileViewExternalSubActions}
155
167
  setGroupByLabels={setGroupByLabels}
168
+ flamechartDimension={flamechartDimension}
169
+ setFlamechartDimension={setFlamechartDimension}
156
170
  showVisualizationSelector={showVisualizationSelector}
157
171
  sandwichFunctionName={sandwichFunctionName}
158
172
  alignFunctionName={alignFunctionName}
@@ -13,8 +13,9 @@
13
13
 
14
14
  import {RpcError} from '@protobuf-ts/runtime-rpc';
15
15
 
16
- import {FlamegraphArrow, QueryServiceClient, Source, TableArrow} from '@parca/client';
16
+ import {FlamegraphArrow, LabelSet, QueryServiceClient, Source, TableArrow} from '@parca/client';
17
17
 
18
+ import {DataPoint} from '../../ProfileFlameChart/SamplesStrips/SamplesGraph';
18
19
  import {ProfileSource} from '../../ProfileSource';
19
20
 
20
21
  export interface FlamegraphData {
@@ -49,6 +50,18 @@ export interface SandwichData {
49
50
  callers: FlamegraphData;
50
51
  }
51
52
 
53
+ export interface SamplesSeries {
54
+ labelset: LabelSet;
55
+ data: DataPoint[];
56
+ }
57
+
58
+ export interface SamplesData {
59
+ loading: boolean;
60
+ series?: SamplesSeries[];
61
+ error: RpcError | null;
62
+ stepMs?: number;
63
+ }
64
+
52
65
  export type VisualizationType =
53
66
  | 'flamegraph'
54
67
  | 'callgraph'
@@ -61,14 +74,15 @@ export interface ProfileViewProps {
61
74
  total: bigint;
62
75
  filtered: bigint;
63
76
  flamegraphData: FlamegraphData;
64
- flamechartData: FlamegraphData;
77
+ samplesData?: SamplesData;
65
78
  sandwichData: SandwichData;
66
79
  topTableData?: TopTableData;
67
80
  sourceData?: SourceData;
68
81
  profileSource: ProfileSource;
69
- queryClient?: QueryServiceClient;
82
+ queryClient: QueryServiceClient;
70
83
  compare?: boolean;
71
84
  onDownloadPProf: () => void;
72
85
  pprofDownloading?: boolean;
73
86
  showVisualizationSelector?: boolean;
87
+ onSwitchToOneMinute?: () => void;
74
88
  }