@parca/profile 0.14.12 → 0.14.13

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 CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.14.13](https://github.com/parca-dev/parca/compare/ui-v0.14.12...ui-v0.14.13) (2022-08-03)
7
+
8
+ ## [0.14.7](https://github.com/parca-dev/parca/compare/ui-v0.14.5...ui-v0.14.7) (2022-07-27)
9
+
10
+ **Note:** Version bump only for package @parca/profile
11
+
6
12
  ## [0.14.12](https://github.com/parca-dev/parca/compare/ui-v0.14.11...ui-v0.14.12) (2022-08-02)
7
13
 
8
14
  **Note:** Version bump only for package @parca/profile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.14.12",
3
+ "version": "0.14.13",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@iconify/react": "^3.2.2",
@@ -22,5 +22,5 @@
22
22
  "access": "public",
23
23
  "registry": "https://registry.npmjs.org/"
24
24
  },
25
- "gitHead": "252b9b4813c78624181ea55614f4339921a2ffc1"
25
+ "gitHead": "614acf00ec750eaee1d52b8e8a827c5c8d54ccfc"
26
26
  }
@@ -1,4 +1,5 @@
1
1
  import React, {MouseEvent, useEffect, useRef, useState} from 'react';
2
+
2
3
  import {throttle} from 'lodash';
3
4
  import {pointer} from 'd3-selection';
4
5
  import {scaleLinear} from 'd3-scale';
@@ -185,7 +186,7 @@ export function IcicleGraphNodes({
185
186
  const onMouseLeave = () => setHoveringNode(undefined);
186
187
 
187
188
  return (
188
- <React.Fragment>
189
+ <React.Fragment key={`node-${key}`}>
189
190
  <IcicleRect
190
191
  key={`rect-${key}`}
191
192
  x={xStart}
@@ -20,13 +20,12 @@ const ProfileIcicleGraph = ({
20
20
  sampleUnit,
21
21
  }: ProfileIcicleGraphProps) => {
22
22
  const compareMode = useAppSelector(selectCompareMode);
23
+ const {ref, dimensions} = useContainerDimensions();
23
24
 
24
25
  if (graph === undefined) return <div>no data...</div>;
25
26
  const total = graph.total;
26
27
  if (parseFloat(total) === 0) return <>Profile has no samples</>;
27
28
 
28
- const {ref, dimensions} = useContainerDimensions();
29
-
30
29
  return (
31
30
  <>
32
31
  {compareMode && <DiffLegend />}
@@ -1,22 +1,46 @@
1
- import React, {useEffect, useState} from 'react';
1
+ import React, {useEffect, useMemo, useState} from 'react';
2
+
2
3
  import {parseParams} from '@parca/functions';
3
- import {QueryServiceClient, QueryRequest_ReportType} from '@parca/client';
4
+ import {QueryServiceClient, Flamegraph, Top} from '@parca/client';
4
5
  import {Button, Card, SearchNodes, useGrpcMetadata, useParcaTheme} from '@parca/components';
5
6
 
6
7
  import ProfileShareButton from './components/ProfileShareButton';
7
8
  import ProfileIcicleGraph from './ProfileIcicleGraph';
8
9
  import {ProfileSource} from './ProfileSource';
9
- import {useQuery} from './useQuery';
10
10
  import TopTable from './TopTable';
11
+ import useDelayedLoader from './useDelayedLoader';
11
12
  import {downloadPprof} from './utils';
12
13
 
13
14
  import './ProfileView.styles.css';
14
15
 
15
16
  type NavigateFunction = (path: string, queryParams: any) => void;
16
17
 
18
+ interface FlamegraphData {
19
+ loading: boolean;
20
+ data?: Flamegraph;
21
+ error?: any;
22
+ }
23
+
24
+ interface TopTableData {
25
+ loading: boolean;
26
+ data?: Top;
27
+ error?: any;
28
+ }
29
+
30
+ type VisualizationType = 'icicle' | 'table' | 'both';
31
+
32
+ interface ProfileVisState {
33
+ currentView: VisualizationType;
34
+ setCurrentView: (view: VisualizationType) => void;
35
+ }
36
+
17
37
  interface ProfileViewProps {
18
- queryClient: QueryServiceClient;
19
- profileSource: ProfileSource;
38
+ flamegraphData?: FlamegraphData;
39
+ topTableData?: TopTableData;
40
+ sampleUnit: string;
41
+ profileVisState: ProfileVisState;
42
+ profileSource?: ProfileSource;
43
+ queryClient?: QueryServiceClient;
20
44
  navigateTo?: NavigateFunction;
21
45
  compare?: boolean;
22
46
  }
@@ -29,22 +53,28 @@ function arrayEquals(a, b): boolean {
29
53
  a.every((val, index) => val === b[index])
30
54
  );
31
55
  }
56
+ export const useProfileVisState = (): ProfileVisState => {
57
+ const router = parseParams(window.location.search);
58
+ const currentViewFromURL = router.currentProfileView as string;
59
+ const [currentView, setCurrentView] = useState<VisualizationType>(
60
+ (currentViewFromURL as VisualizationType) || 'icicle'
61
+ );
62
+
63
+ return {currentView, setCurrentView};
64
+ };
32
65
 
33
66
  export const ProfileView = ({
34
- queryClient,
67
+ flamegraphData,
68
+ topTableData,
69
+ sampleUnit,
35
70
  profileSource,
71
+ queryClient,
36
72
  navigateTo,
73
+ profileVisState,
37
74
  }: ProfileViewProps): JSX.Element => {
38
- const router = parseParams(window.location.search);
39
- const currentViewFromURL = router.currentProfileView as string;
40
75
  const [curPath, setCurPath] = useState<string[]>([]);
41
- const [isLoaderVisible, setIsLoaderVisible] = useState<boolean>(false);
42
- const {isLoading, response, error} = useQuery(
43
- queryClient,
44
- profileSource,
45
- QueryRequest_ReportType.FLAMEGRAPH_UNSPECIFIED
46
- );
47
- const [currentView, setCurrentView] = useState<string | undefined>(currentViewFromURL);
76
+ const {currentView, setCurrentView} = profileVisState;
77
+
48
78
  const metadata = useGrpcMetadata();
49
79
  const {loader} = useParcaTheme();
50
80
 
@@ -53,29 +83,39 @@ export const ProfileView = ({
53
83
  setCurPath([]);
54
84
  }, [profileSource]);
55
85
 
56
- useEffect(() => {
57
- let showLoaderTimeout;
58
- if (isLoading && !isLoaderVisible) {
59
- // if the request takes longer than half a second, show the loading icon
60
- showLoaderTimeout = setTimeout(() => {
61
- setIsLoaderVisible(true);
62
- }, 500);
63
- } else {
64
- setIsLoaderVisible(false);
86
+ const isLoading = useMemo(() => {
87
+ if (currentView === 'icicle') {
88
+ return !!flamegraphData?.loading;
65
89
  }
66
- return () => clearTimeout(showLoaderTimeout);
67
- }, [isLoading]);
90
+ if (currentView === 'table') {
91
+ return !!topTableData?.loading;
92
+ }
93
+ if (currentView === 'both') {
94
+ return !!flamegraphData?.loading || !!topTableData?.loading;
95
+ }
96
+ return false;
97
+ }, [currentView, flamegraphData?.loading, topTableData?.loading]);
98
+
99
+ const isLoaderVisible = useDelayedLoader(isLoading);
68
100
 
69
101
  if (isLoaderVisible) {
70
102
  return <>{loader}</>;
71
103
  }
72
104
 
73
- if (error !== null) {
74
- return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
105
+ if (flamegraphData?.error != null) {
106
+ console.error('Error: ', flamegraphData?.error);
107
+ return (
108
+ <div className="p-10 flex justify-center">
109
+ An error occurred: {flamegraphData?.error.message}
110
+ </div>
111
+ );
75
112
  }
76
113
 
77
114
  const downloadPProf = async (e: React.MouseEvent<HTMLElement>) => {
78
115
  e.preventDefault();
116
+ if (!profileSource || !queryClient) {
117
+ return;
118
+ }
79
119
 
80
120
  try {
81
121
  const blob = await downloadPprof(profileSource.QueryRequest(), queryClient, metadata);
@@ -96,16 +136,18 @@ export const ProfileView = ({
96
136
  }
97
137
  };
98
138
 
99
- const switchProfileView = (view: string) => {
139
+ const switchProfileView = (view: VisualizationType) => {
140
+ if (view == null) {
141
+ return;
142
+ }
100
143
  if (navigateTo === undefined) return;
101
144
 
102
145
  setCurrentView(view);
146
+ const router = parseParams(window.location.search);
103
147
 
104
148
  navigateTo('/', {...router, ...{currentProfileView: view}});
105
149
  };
106
150
 
107
- const sampleUnit = profileSource.ProfileType().sampleUnit;
108
-
109
151
  return (
110
152
  <>
111
153
  <div className="py-3">
@@ -114,10 +156,12 @@ export const ProfileView = ({
114
156
  <div className="flex py-3 w-full">
115
157
  <div className="w-2/5 flex space-x-4">
116
158
  <div className="flex space-x-1">
117
- <ProfileShareButton
118
- queryRequest={profileSource.QueryRequest()}
119
- queryClient={queryClient}
120
- />
159
+ {profileSource && queryClient ? (
160
+ <ProfileShareButton
161
+ queryRequest={profileSource.QueryRequest()}
162
+ queryClient={queryClient}
163
+ />
164
+ ) : null}
121
165
 
122
166
  <Button color="neutral" onClick={downloadPProf}>
123
167
  Download pprof
@@ -149,7 +193,7 @@ export const ProfileView = ({
149
193
 
150
194
  <Button
151
195
  variant={`${currentView === 'both' ? 'primary' : 'neutral'}`}
152
- 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 no-outline-on-buttons text-ellipsis"
196
+ 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"
153
197
  onClick={() => switchProfileView('both')}
154
198
  >
155
199
  Both
@@ -166,45 +210,35 @@ export const ProfileView = ({
166
210
  </div>
167
211
 
168
212
  <div className="flex space-x-4 justify-between">
169
- {currentView === 'icicle' &&
170
- response !== null &&
171
- response.report.oneofKind === 'flamegraph' && (
172
- <div className="w-full">
173
- <ProfileIcicleGraph
174
- curPath={curPath}
175
- setNewCurPath={setNewCurPath}
176
- graph={response.report.flamegraph}
177
- sampleUnit={sampleUnit}
178
- />
179
- </div>
180
- )}
181
-
182
- {currentView === 'table' && (
213
+ {currentView === 'icicle' && flamegraphData?.data != null && (
183
214
  <div className="w-full">
184
- <TopTable
185
- queryClient={queryClient}
186
- profileSource={profileSource}
215
+ <ProfileIcicleGraph
216
+ curPath={curPath}
217
+ setNewCurPath={setNewCurPath}
218
+ graph={flamegraphData.data}
187
219
  sampleUnit={sampleUnit}
188
220
  />
189
221
  </div>
190
222
  )}
191
223
 
224
+ {currentView === 'table' && topTableData != null && (
225
+ <div className="w-full">
226
+ <TopTable data={topTableData.data} sampleUnit={sampleUnit} />
227
+ </div>
228
+ )}
229
+
192
230
  {currentView === 'both' && (
193
231
  <>
194
232
  <div className="w-1/2">
195
- <TopTable
196
- queryClient={queryClient}
197
- profileSource={profileSource}
198
- sampleUnit={sampleUnit}
199
- />
233
+ <TopTable data={topTableData?.data} sampleUnit={sampleUnit} />
200
234
  </div>
201
235
 
202
236
  <div className="w-1/2">
203
- {response !== null && response.report.oneofKind === 'flamegraph' && (
237
+ {flamegraphData != null && (
204
238
  <ProfileIcicleGraph
205
239
  curPath={curPath}
206
240
  setNewCurPath={setNewCurPath}
207
- graph={response.report.flamegraph}
241
+ graph={flamegraphData.data}
208
242
  sampleUnit={sampleUnit}
209
243
  />
210
244
  )}
@@ -0,0 +1,66 @@
1
+ import {QueryServiceClient, QueryRequest_ReportType} from '@parca/client';
2
+
3
+ import {useQuery} from './useQuery';
4
+ import {ProfileView, useProfileVisState} from './ProfileView';
5
+ import {ProfileSource} from './ProfileSource';
6
+
7
+ type NavigateFunction = (path: string, queryParams: any) => void;
8
+
9
+ interface ProfileViewWithDataProps {
10
+ queryClient: QueryServiceClient;
11
+ profileSource: ProfileSource;
12
+ navigateTo?: NavigateFunction;
13
+ compare?: boolean;
14
+ }
15
+
16
+ export const ProfileViewWithData = ({
17
+ queryClient,
18
+ profileSource,
19
+ navigateTo,
20
+ }: ProfileViewWithDataProps) => {
21
+ const profileVisState = useProfileVisState();
22
+ const {currentView} = profileVisState;
23
+ const {
24
+ isLoading: flamegraphLoading,
25
+ response: flamegraphResponse,
26
+ error: flamegraphError,
27
+ } = useQuery(queryClient, profileSource, QueryRequest_ReportType.FLAMEGRAPH_UNSPECIFIED, {
28
+ skip: currentView != 'icicle' && currentView != 'both',
29
+ });
30
+
31
+ const {
32
+ isLoading: topTableLoading,
33
+ response: topTableResponse,
34
+ error: topTableError,
35
+ } = useQuery(queryClient, profileSource, QueryRequest_ReportType.TOP, {
36
+ skip: currentView != 'table' && currentView != 'both',
37
+ });
38
+
39
+ const sampleUnit = profileSource.ProfileType().sampleUnit;
40
+
41
+ return (
42
+ <ProfileView
43
+ flamegraphData={{
44
+ loading: flamegraphLoading,
45
+ data:
46
+ flamegraphResponse?.report.oneofKind === 'flamegraph'
47
+ ? flamegraphResponse?.report?.flamegraph
48
+ : undefined,
49
+ error: flamegraphError,
50
+ }}
51
+ topTableData={{
52
+ loading: topTableLoading,
53
+ data:
54
+ topTableResponse?.report.oneofKind === 'top' ? topTableResponse.report.top : undefined,
55
+ error: topTableError,
56
+ }}
57
+ profileVisState={profileVisState}
58
+ sampleUnit={sampleUnit}
59
+ profileSource={profileSource}
60
+ queryClient={queryClient}
61
+ navigateTo={navigateTo}
62
+ />
63
+ );
64
+ };
65
+
66
+ export default ProfileViewWithData;
package/src/TopTable.tsx CHANGED
@@ -1,21 +1,15 @@
1
1
  import React from 'react';
2
- import {getLastItem, valueFormatter, isSearchMatch, SEARCH_STRING_COLOR} from '@parca/functions';
2
+
3
+ import {getLastItem, valueFormatter, isSearchMatch} from '@parca/functions';
3
4
  import {useAppSelector, selectCompareMode, selectSearchNodeString} from '@parca/store';
4
- import {
5
- QueryResponse,
6
- QueryServiceClient,
7
- QueryRequest_ReportType,
8
- TopNodeMeta,
9
- } from '@parca/client';
10
- import {ProfileSource} from './ProfileSource';
11
- import {useQuery} from './useQuery';
5
+ import {TopNodeMeta, Top} from '@parca/client';
6
+
12
7
  import {hexifyAddress} from './utils';
13
8
 
14
9
  import './TopTable.styles.css';
15
10
 
16
- interface ProfileViewProps {
17
- queryClient: QueryServiceClient;
18
- profileSource: ProfileSource;
11
+ interface TopTableProps {
12
+ data?: Top;
19
13
  sampleUnit: string;
20
14
  }
21
15
 
@@ -29,21 +23,17 @@ const Arrow = ({direction}: {direction: string | undefined}) => {
29
23
  width="11"
30
24
  xmlns="http://www.w3.org/2000/svg"
31
25
  >
32
- <path clip-rule="evenodd" d="m.573997 0 5.000003 10 5-10h-9.999847z" fill-rule="evenodd" />
26
+ <path clipRule="evenodd" d="m.573997 0 5.000003 10 5-10h-9.999847z" fillRule="evenodd" />
33
27
  </svg>
34
28
  );
35
29
  };
36
30
 
37
- const useSortableData = (
38
- response: QueryResponse | null,
39
- config = {key: 'cumulative', direction: 'desc'}
40
- ) => {
31
+ const useSortableData = (top?: Top, config = {key: 'cumulative', direction: 'desc'}) => {
41
32
  const [sortConfig, setSortConfig] = React.useState<{key: string; direction: string} | null>(
42
33
  config
43
34
  );
44
35
 
45
- const rawTableReport =
46
- response !== null && response.report.oneofKind === 'top' ? response.report.top.list : [];
36
+ const rawTableReport = top ? top.list : [];
47
37
 
48
38
  const items = rawTableReport.map(node => ({
49
39
  ...node,
@@ -98,25 +88,15 @@ export const RowLabel = (meta: TopNodeMeta | undefined): string => {
98
88
  return fallback === '' ? '<unknown>' : fallback;
99
89
  };
100
90
 
101
- export const TopTable = ({
102
- queryClient,
103
- profileSource,
104
- sampleUnit,
105
- }: ProfileViewProps): JSX.Element => {
106
- const {response, error} = useQuery(queryClient, profileSource, QueryRequest_ReportType.TOP);
107
- const {items, requestSort, sortConfig} = useSortableData(response);
91
+ export const TopTable = ({data: top, sampleUnit}: TopTableProps): JSX.Element => {
92
+ const {items, requestSort, sortConfig} = useSortableData(top);
108
93
  const currentSearchString = useAppSelector(selectSearchNodeString);
109
94
 
110
95
  const compareMode = useAppSelector(selectCompareMode);
111
96
 
112
97
  const unit = sampleUnit;
113
98
 
114
- if (error != null) {
115
- return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
116
- }
117
-
118
- const total =
119
- response !== null && response.report.oneofKind === 'top' ? response.report.top.list.length : 0;
99
+ const total = top ? top.list.length : 0;
120
100
  if (total === 0) return <>Profile has no samples</>;
121
101
 
122
102
  const getClassNamesFor = name => {
package/src/index.tsx CHANGED
@@ -2,4 +2,5 @@ export * from './IcicleGraph';
2
2
  export * from './ProfileIcicleGraph';
3
3
  export * from './ProfileSource';
4
4
  export * from './ProfileView';
5
+ export * from './ProfileViewWithData';
5
6
  export * from './utils';
@@ -0,0 +1,26 @@
1
+ import {useEffect, useState} from 'react';
2
+
3
+ interface DelayedLoaderOptions {
4
+ delay?: number;
5
+ }
6
+
7
+ const useDelayedLoader = (isLoading = false, options?: DelayedLoaderOptions) => {
8
+ const {delay = 500} = options || {};
9
+ const [isLoaderVisible, setIsLoaderVisible] = useState<boolean>(false);
10
+ useEffect(() => {
11
+ let showLoaderTimeout;
12
+ if (isLoading && !isLoaderVisible) {
13
+ // if the request takes longer than half a second, show the loading icon
14
+ showLoaderTimeout = setTimeout(() => {
15
+ setIsLoaderVisible(true);
16
+ }, delay);
17
+ } else {
18
+ setIsLoaderVisible(false);
19
+ }
20
+ return () => clearTimeout(showLoaderTimeout);
21
+ }, [isLoading]);
22
+
23
+ return isLoaderVisible;
24
+ };
25
+
26
+ export default useDelayedLoader;
package/src/useQuery.tsx CHANGED
@@ -1,7 +1,9 @@
1
- import React, {useEffect, useState} from 'react';
1
+ import {useEffect, useState} from 'react';
2
+
2
3
  import {QueryServiceClient, QueryResponse, QueryRequest_ReportType} from '@parca/client';
3
4
  import {RpcError} from '@protobuf-ts/runtime-rpc';
4
5
  import {useGrpcMetadata} from '@parca/components';
6
+
5
7
  import {ProfileSource} from './ProfileSource';
6
8
 
7
9
  export interface IQueryResult {
@@ -10,11 +12,17 @@ export interface IQueryResult {
10
12
  isLoading: boolean;
11
13
  }
12
14
 
15
+ interface UseQueryOptions {
16
+ skip?: boolean;
17
+ }
18
+
13
19
  export const useQuery = (
14
20
  client: QueryServiceClient,
15
21
  profileSource: ProfileSource,
16
- reportType: QueryRequest_ReportType
22
+ reportType: QueryRequest_ReportType,
23
+ options?: UseQueryOptions
17
24
  ): IQueryResult => {
25
+ const {skip = false} = options || {};
18
26
  const [result, setResult] = useState<IQueryResult>({
19
27
  response: null,
20
28
  error: null,
@@ -23,6 +31,9 @@ export const useQuery = (
23
31
  const metadata = useGrpcMetadata();
24
32
 
25
33
  useEffect(() => {
34
+ if (skip) {
35
+ return;
36
+ }
26
37
  setResult({
27
38
  response: null,
28
39
  error: null,