@parca/profile 0.16.91 → 0.16.93

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.
@@ -14,32 +14,26 @@
14
14
  import {Profiler, useEffect, useMemo, useState} from 'react';
15
15
  import {scaleLinear} from 'd3';
16
16
 
17
- import {getNewSpanColor, parseParams} from '@parca/functions';
18
- import useUIFeatureFlag from '@parca/functions/useUIFeatureFlag';
17
+ import cx from 'classnames';
18
+ import {getNewSpanColor, useURLState} from '@parca/functions';
19
+ import {CloseIcon} from '@parca/icons';
19
20
  import {QueryServiceClient, Flamegraph, Top, Callgraph as CallgraphType} from '@parca/client';
20
21
  import {Button, Card, useParcaContext} from '@parca/components';
21
22
  import {useContainerDimensions} from '@parca/dynamicsize';
22
- import {
23
- useAppSelector,
24
- selectDarkMode,
25
- selectSearchNodeString,
26
- selectFilterByFunction,
27
- useAppDispatch,
28
- setSearchNodeString,
29
- } from '@parca/store';
23
+ import {useAppSelector, selectDarkMode} from '@parca/store';
30
24
 
31
25
  import {Callgraph} from '../';
32
26
  import ProfileShareButton from '../components/ProfileShareButton';
33
27
  import FilterByFunctionButton from './FilterByFunctionButton';
28
+ import ViewSelector from './ViewSelector';
34
29
  import ProfileIcicleGraph, {ResizeHandler} from '../ProfileIcicleGraph';
35
30
  import {ProfileSource} from '../ProfileSource';
36
31
  import TopTable from '../TopTable';
37
32
  import useDelayedLoader from '../useDelayedLoader';
38
33
 
39
34
  import '../ProfileView.styles.css';
40
- import useUserPreference, {USER_PREFERENCES} from '@parca/functions/useUserPreference';
41
35
 
42
- type NavigateFunction = (path: string, queryParams: any) => void;
36
+ type NavigateFunction = (path: string, queryParams: any, options?: {replace?: boolean}) => void;
43
37
 
44
38
  export interface FlamegraphData {
45
39
  loading: boolean;
@@ -59,19 +53,11 @@ interface CallgraphData {
59
53
  error?: any;
60
54
  }
61
55
 
62
- export type VisualizationType = 'icicle' | 'table' | 'callgraph' | 'both';
63
-
64
- export interface ProfileVisState {
65
- currentView: VisualizationType;
66
- setCurrentView: (view: VisualizationType) => void;
67
- }
68
-
69
56
  export interface ProfileViewProps {
70
57
  flamegraphData?: FlamegraphData;
71
58
  topTableData?: TopTableData;
72
59
  callgraphData?: CallgraphData;
73
60
  sampleUnit: string;
74
- profileVisState: ProfileVisState;
75
61
  profileSource?: ProfileSource;
76
62
  queryClient?: QueryServiceClient;
77
63
  navigateTo?: NavigateFunction;
@@ -88,22 +74,6 @@ function arrayEquals<T>(a: T[], b: T[]): boolean {
88
74
  a.every((val, index) => val === b[index])
89
75
  );
90
76
  }
91
- export const useProfileVisState = (): ProfileVisState => {
92
- const [currentView, setCurrentView] = useState<VisualizationType>(() => {
93
- if (typeof window === 'undefined') {
94
- return 'icicle';
95
- }
96
- const router = parseParams(window.location.search);
97
- const currentViewFromURL = router.currentProfileView as string;
98
-
99
- if (currentViewFromURL != null) {
100
- return currentViewFromURL as VisualizationType;
101
- }
102
- return 'icicle';
103
- });
104
-
105
- return {currentView, setCurrentView};
106
- };
107
77
 
108
78
  export const ProfileView = ({
109
79
  flamegraphData,
@@ -113,22 +83,18 @@ export const ProfileView = ({
113
83
  profileSource,
114
84
  queryClient,
115
85
  navigateTo,
116
- profileVisState,
117
86
  onDownloadPProf,
118
87
  onFlamegraphContainerResize,
119
88
  }: ProfileViewProps): JSX.Element => {
120
- const dispatch = useAppDispatch();
121
89
  const {ref, dimensions} = useContainerDimensions();
122
90
  const [curPath, setCurPath] = useState<string[]>([]);
123
- const {currentView, setCurrentView} = profileVisState;
91
+ const [rawDashboardItems, setDashboardItems] = useURLState({
92
+ param: 'dashboard_items',
93
+ navigateTo,
94
+ });
95
+ const dashboardItems = rawDashboardItems as string[];
124
96
  const isDarkMode = useAppSelector(selectDarkMode);
125
- const currentSearchString = useAppSelector(selectSearchNodeString);
126
- const filterByFunctionString = useAppSelector(selectFilterByFunction);
127
-
128
- const [callgraphEnabled] = useUIFeatureFlag('callgraph');
129
- const [highlightAfterFilteringEnabled] = useUserPreference<boolean>(
130
- USER_PREFERENCES.HIGHTLIGHT_AFTER_FILTERING.key
131
- );
97
+ const isSinglePanelView = dashboardItems.length === 1;
132
98
 
133
99
  const {loader, perf} = useParcaContext();
134
100
 
@@ -138,42 +104,17 @@ export const ProfileView = ({
138
104
  }, [profileSource]);
139
105
 
140
106
  const isLoading = useMemo(() => {
141
- if (currentView === 'icicle') {
107
+ if (dashboardItems.includes('icicle')) {
142
108
  return Boolean(flamegraphData?.loading);
143
109
  }
144
- if (currentView === 'callgraph') {
110
+ if (dashboardItems.includes('callgraph')) {
145
111
  return Boolean(callgraphData?.loading);
146
112
  }
147
- if (currentView === 'table') {
113
+ if (dashboardItems.includes('table')) {
148
114
  return Boolean(topTableData?.loading);
149
115
  }
150
- if (currentView === 'both') {
151
- return Boolean(flamegraphData?.loading) || Boolean(topTableData?.loading);
152
- }
153
116
  return false;
154
- }, [currentView, callgraphData?.loading, flamegraphData?.loading, topTableData?.loading]);
155
-
156
- useEffect(() => {
157
- if (!highlightAfterFilteringEnabled) {
158
- if (currentSearchString !== undefined && currentSearchString !== '') {
159
- dispatch(setSearchNodeString(''));
160
- }
161
- return;
162
- }
163
- if (isLoading) {
164
- return;
165
- }
166
- if (filterByFunctionString === currentSearchString) {
167
- return;
168
- }
169
- dispatch(setSearchNodeString(filterByFunctionString));
170
- }, [
171
- isLoading,
172
- filterByFunctionString,
173
- dispatch,
174
- highlightAfterFilteringEnabled,
175
- currentSearchString,
176
- ]);
117
+ }, [dashboardItems, callgraphData?.loading, flamegraphData?.loading, topTableData?.loading]);
177
118
 
178
119
  const isLoaderVisible = useDelayedLoader(isLoading);
179
120
 
@@ -186,36 +127,94 @@ export const ProfileView = ({
186
127
  );
187
128
  }
188
129
 
189
- const resetIcicleGraph = (): void => setCurPath([]);
190
-
191
130
  const setNewCurPath = (path: string[]): void => {
192
131
  if (!arrayEquals(curPath, path)) {
193
132
  setCurPath(path);
194
133
  }
195
134
  };
196
135
 
197
- const switchProfileView = (view: VisualizationType): void => {
198
- if (view == null) {
199
- return;
200
- }
201
- setCurrentView(view);
136
+ const maxColor: string = getNewSpanColor(isDarkMode);
137
+ const minColor: string = scaleLinear([isDarkMode ? 'black' : 'white', maxColor])(0.3);
138
+ const colorRange: [string, string] = [minColor, maxColor];
202
139
 
203
- if (navigateTo === undefined) {
204
- return;
140
+ const getDashboardItemByType = ({
141
+ type,
142
+ isHalfScreen,
143
+ }: {
144
+ type: string;
145
+ isHalfScreen: boolean;
146
+ }): JSX.Element => {
147
+ switch (type) {
148
+ case 'icicle': {
149
+ return flamegraphData?.data != null ? (
150
+ <Profiler id="icicleGraph" onRender={perf?.onRender as React.ProfilerOnRenderCallback}>
151
+ <ProfileIcicleGraph
152
+ curPath={curPath}
153
+ setNewCurPath={setNewCurPath}
154
+ graph={flamegraphData.data}
155
+ sampleUnit={sampleUnit}
156
+ onContainerResize={onFlamegraphContainerResize}
157
+ />
158
+ </Profiler>
159
+ ) : (
160
+ <></>
161
+ );
162
+ }
163
+ case 'callgraph': {
164
+ return callgraphData?.data != null && dimensions?.width !== undefined ? (
165
+ <Callgraph
166
+ graph={callgraphData.data}
167
+ sampleUnit={sampleUnit}
168
+ width={isHalfScreen ? dimensions?.width / 2 : dimensions?.width}
169
+ colorRange={colorRange}
170
+ />
171
+ ) : (
172
+ <></>
173
+ );
174
+ }
175
+ case 'table': {
176
+ return topTableData != null ? (
177
+ <TopTable data={topTableData.data} sampleUnit={sampleUnit} navigateTo={navigateTo} />
178
+ ) : (
179
+ <></>
180
+ );
181
+ }
182
+ default: {
183
+ return <></>;
184
+ }
205
185
  }
206
- const router = parseParams(window.location.search);
207
- navigateTo('/', {
208
- ...router,
209
- ...{currentProfileView: view},
210
- ...{searchString: currentSearchString},
211
- });
212
186
  };
213
187
 
214
- const maxColor: string = getNewSpanColor(isDarkMode);
215
- // TODO: fix colors for dark mode
216
- const minColor: string = scaleLinear([isDarkMode ? 'black' : 'white', maxColor])(0.3);
188
+ const handleResetView = (): void => {
189
+ setDashboardItems(['icicle']);
190
+ };
217
191
 
218
- const colorRange: [string, string] = [minColor, maxColor];
192
+ const handleClosePanel = (visualizationType: string): void => {
193
+ const newDashboardItems = dashboardItems.filter(item => item !== visualizationType);
194
+ setDashboardItems(newDashboardItems);
195
+ };
196
+
197
+ const dashboardItemsWithViewSelector = dashboardItems.map((dashboardItem, index) => {
198
+ return (
199
+ <div
200
+ key={index}
201
+ className={cx(
202
+ 'border dark:bg-gray-700 rounded border-gray-300 dark:border-gray-500 p-3',
203
+ isSinglePanelView ? 'w-full' : 'w-1/2'
204
+ )}
205
+ >
206
+ <div className="w-full flex justify-end pb-2">
207
+ <ViewSelector defaultValue={dashboardItem} navigateTo={navigateTo} position={index} />
208
+ {!isSinglePanelView && (
209
+ <button type="button" onClick={() => handleClosePanel(dashboardItem)} className="pl-2">
210
+ <CloseIcon />
211
+ </button>
212
+ )}
213
+ </div>
214
+ {getDashboardItemByType({type: dashboardItem, isHalfScreen: !isSinglePanelView})}
215
+ </div>
216
+ );
217
+ });
219
218
 
220
219
  return (
221
220
  <>
@@ -244,54 +243,28 @@ export const ProfileView = ({
244
243
  Download pprof
245
244
  </Button>
246
245
  </div>
247
- <FilterByFunctionButton />
246
+ <FilterByFunctionButton navigateTo={navigateTo} />
248
247
  </div>
249
248
 
250
249
  <div className="flex ml-auto gap-2">
251
250
  <Button
252
251
  color="neutral"
253
- onClick={resetIcicleGraph}
254
- disabled={curPath.length === 0}
252
+ onClick={handleResetView}
253
+ disabled={isSinglePanelView}
255
254
  className="whitespace-nowrap text-ellipsis"
256
255
  >
257
- Reset View
256
+ Reset Panels
258
257
  </Button>
259
258
 
260
- {callgraphEnabled ? (
261
- <Button
262
- variant={`${currentView === 'callgraph' ? 'primary' : 'neutral'}`}
263
- onClick={() => switchProfileView('callgraph')}
264
- className="whitespace-nowrap text-ellipsis"
265
- >
266
- Callgraph
267
- </Button>
268
- ) : null}
269
-
270
- <div className="flex">
271
- <Button
272
- variant={`${currentView === 'table' ? 'primary' : 'neutral'}`}
273
- className="items-center rounded-tr-none rounded-br-none w-auto px-8 whitespace-nowrap text-ellipsis no-outline-on-buttons"
274
- onClick={() => switchProfileView('table')}
275
- >
276
- Table
277
- </Button>
278
-
279
- <Button
280
- variant={`${currentView === 'both' ? 'primary' : 'neutral'}`}
281
- className="items-center rounded-tl-none rounded-tr-none rounded-bl-none rounded-br-none border-l-0 border-r-0 w-auto px-8 whitespace-nowrap no-outline-on-buttons text-ellipsis"
282
- onClick={() => switchProfileView('both')}
283
- >
284
- Both
285
- </Button>
286
-
287
- <Button
288
- variant={`${currentView === 'icicle' ? 'primary' : 'neutral'}`}
289
- className="items-center rounded-tl-none rounded-bl-none w-auto px-8 whitespace-nowrap text-ellipsis no-outline-on-buttons"
290
- onClick={() => switchProfileView('icicle')}
291
- >
292
- Icicle Graph
293
- </Button>
294
- </div>
259
+ <ViewSelector
260
+ defaultValue=""
261
+ navigateTo={navigateTo}
262
+ position={-1}
263
+ placeholderText="Add panel..."
264
+ primary
265
+ addView={true}
266
+ disabled={!isSinglePanelView || dashboardItems.length < 1}
267
+ />
295
268
  </div>
296
269
  </div>
297
270
 
@@ -299,57 +272,7 @@ export const ProfileView = ({
299
272
  <>{loader}</>
300
273
  ) : (
301
274
  <div ref={ref} className="flex space-x-4 justify-between w-full">
302
- {currentView === 'icicle' && flamegraphData?.data != null && (
303
- <div className="w-full">
304
- <Profiler
305
- id="icicleGraph"
306
- onRender={perf?.onRender as React.ProfilerOnRenderCallback}
307
- >
308
- <ProfileIcicleGraph
309
- curPath={curPath}
310
- setNewCurPath={setNewCurPath}
311
- graph={flamegraphData.data}
312
- sampleUnit={sampleUnit}
313
- onContainerResize={onFlamegraphContainerResize}
314
- />
315
- </Profiler>
316
- </div>
317
- )}
318
- {currentView === 'callgraph' && callgraphData?.data != null && (
319
- <div className="w-full">
320
- {dimensions?.width !== undefined && (
321
- <Callgraph
322
- graph={callgraphData.data}
323
- sampleUnit={sampleUnit}
324
- width={dimensions?.width}
325
- colorRange={colorRange}
326
- />
327
- )}
328
- </div>
329
- )}
330
- {currentView === 'table' && topTableData != null && (
331
- <div className="w-full">
332
- <TopTable data={topTableData.data} sampleUnit={sampleUnit} />
333
- </div>
334
- )}
335
- {currentView === 'both' && (
336
- <>
337
- <div className="w-1/2">
338
- <TopTable data={topTableData?.data} sampleUnit={sampleUnit} />
339
- </div>
340
-
341
- <div className="w-1/2">
342
- {flamegraphData != null && (
343
- <ProfileIcicleGraph
344
- curPath={curPath}
345
- setNewCurPath={setNewCurPath}
346
- graph={flamegraphData.data}
347
- sampleUnit={sampleUnit}
348
- />
349
- )}
350
- </div>
351
- </>
352
- )}
275
+ {dashboardItemsWithViewSelector}
353
276
  </div>
354
277
  )}
355
278
  </Card.Body>
@@ -11,19 +11,16 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
+ import {useEffect, useState} from 'react';
14
15
  import {QueryServiceClient, QueryRequest_ReportType} from '@parca/client';
15
-
16
16
  import {useQuery} from './useQuery';
17
- import {ProfileView, useProfileVisState} from './ProfileView';
17
+ import {ProfileView} from './ProfileView';
18
18
  import {ProfileSource} from './ProfileSource';
19
19
  import {downloadPprof} from './utils';
20
20
  import {useGrpcMetadata, useParcaContext} from '@parca/components';
21
- import {saveAsBlob} from '@parca/functions';
22
- import {useEffect, useState} from 'react';
21
+ import {saveAsBlob, NavigateFunction, useURLState} from '@parca/functions';
23
22
  import useUserPreference, {USER_PREFERENCES} from '@parca/functions/useUserPreference';
24
23
 
25
- export type NavigateFunction = (path: string, queryParams: any) => void;
26
-
27
24
  interface ProfileViewWithDataProps {
28
25
  queryClient: QueryServiceClient;
29
26
  profileSource: ProfileSource;
@@ -36,9 +33,8 @@ export const ProfileViewWithData = ({
36
33
  profileSource,
37
34
  navigateTo,
38
35
  }: ProfileViewWithDataProps): JSX.Element => {
39
- const profileVisState = useProfileVisState();
40
36
  const metadata = useGrpcMetadata();
41
- const {currentView} = profileVisState;
37
+ const [dashboardItems] = useURLState({param: 'dashboard_items', navigateTo});
42
38
  const [nodeTrimThreshold, setNodeTrimThreshold] = useState<number>(0);
43
39
  const [disableTrimming] = useUserPreference<boolean>(USER_PREFERENCES.DISABLE_GRAPH_TRIMMING.key);
44
40
 
@@ -64,7 +60,7 @@ export const ProfileViewWithData = ({
64
60
  response: flamegraphResponse,
65
61
  error: flamegraphError,
66
62
  } = useQuery(queryClient, profileSource, QueryRequest_ReportType.FLAMEGRAPH_TABLE, {
67
- skip: currentView !== 'icicle' && currentView !== 'both',
63
+ skip: !dashboardItems.includes('icicle'),
68
64
  nodeTrimThreshold,
69
65
  });
70
66
  const {perf} = useParcaContext();
@@ -86,7 +82,7 @@ export const ProfileViewWithData = ({
86
82
  response: topTableResponse,
87
83
  error: topTableError,
88
84
  } = useQuery(queryClient, profileSource, QueryRequest_ReportType.TOP, {
89
- skip: currentView !== 'table' && currentView !== 'both',
85
+ skip: !dashboardItems.includes('table'),
90
86
  });
91
87
 
92
88
  const {
@@ -94,7 +90,7 @@ export const ProfileViewWithData = ({
94
90
  response: callgraphResponse,
95
91
  error: callgraphError,
96
92
  } = useQuery(queryClient, profileSource, QueryRequest_ReportType.CALLGRAPH, {
97
- skip: currentView !== 'callgraph',
93
+ skip: !dashboardItems.includes('callgraph'),
98
94
  });
99
95
 
100
96
  const sampleUnit = profileSource.ProfileType().sampleUnit;
@@ -136,7 +132,6 @@ export const ProfileViewWithData = ({
136
132
  : undefined,
137
133
  error: callgraphError,
138
134
  }}
139
- profileVisState={profileVisState}
140
135
  sampleUnit={sampleUnit}
141
136
  profileSource={profileSource}
142
137
  queryClient={queryClient}
@@ -13,14 +13,14 @@
13
13
 
14
14
  import React, {useCallback, useMemo} from 'react';
15
15
 
16
- import {getLastItem, valueFormatter, isSearchMatch} from '@parca/functions';
17
16
  import {
18
- useAppSelector,
19
- selectCompareMode,
20
- selectSearchNodeString,
21
- setSearchNodeString,
22
- useAppDispatch,
23
- } from '@parca/store';
17
+ getLastItem,
18
+ valueFormatter,
19
+ isSearchMatch,
20
+ NavigateFunction,
21
+ parseParams,
22
+ selectQueryParam,
23
+ } from '@parca/functions';
24
24
  import {TopNode, TopNodeMeta, Top} from '@parca/client';
25
25
  import {Table} from '@parca/components';
26
26
  import {createColumnHelper, ColumnDef} from '@tanstack/react-table';
@@ -32,6 +32,7 @@ import '../TopTable.styles.css';
32
32
  interface TopTableProps {
33
33
  data?: Top;
34
34
  sampleUnit: string;
35
+ navigateTo?: NavigateFunction;
35
36
  }
36
37
 
37
38
  export const RowLabel = (meta: TopNodeMeta | undefined): string => {
@@ -60,10 +61,11 @@ const addPlusSign = (num: string): string => {
60
61
  return `+${num}`;
61
62
  };
62
63
 
63
- export const TopTable = ({data: top, sampleUnit: unit}: TopTableProps): JSX.Element => {
64
- const currentSearchString = useAppSelector(selectSearchNodeString);
65
- const compareMode = useAppSelector(selectCompareMode);
66
- const dispatch = useAppDispatch();
64
+ export const TopTable = ({data: top, sampleUnit: unit, navigateTo}: TopTableProps): JSX.Element => {
65
+ const router = parseParams(window.location.search);
66
+ const currentSearchString = selectQueryParam('search_string') as string;
67
+ const compareMode =
68
+ Boolean(selectQueryParam('compare_a')) && Boolean(selectQueryParam('compare_b'));
67
69
 
68
70
  const columns = React.useMemo(() => {
69
71
  const cols: Array<ColumnDef<TopNode, any>> = [
@@ -117,9 +119,18 @@ export const TopTable = ({data: top, sampleUnit: unit}: TopTableProps): JSX.Elem
117
119
 
118
120
  const selectSpan = useCallback(
119
121
  (span: string): void => {
120
- dispatch(setSearchNodeString(span.trim()));
122
+ if (navigateTo != null) {
123
+ navigateTo(
124
+ '/',
125
+ {
126
+ ...router,
127
+ ...{search_string: span.trim()},
128
+ },
129
+ {replace: true}
130
+ );
131
+ }
121
132
  },
122
- [dispatch]
133
+ [navigateTo, router]
123
134
  );
124
135
 
125
136
  const onRowClick = useCallback(