@parca/profile 0.8.0 → 0.9.2

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,26 @@
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.9.2](https://github.com/parca-dev/parca/compare/ui-v0.9.1...ui-v0.9.2) (2022-02-22)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
10
+ ## [0.9.1](https://github.com/parca-dev/parca/compare/ui-v0.9.0...ui-v0.9.1) (2022-02-21)
11
+
12
+ ## [0.8.2](https://github.com/parca-dev/parca/compare/ui-v0.8.1...ui-v0.8.2) (2022-02-14)
13
+
14
+ **Note:** Version bump only for package @parca/profile
15
+
16
+ # [0.9.0](https://github.com/parca-dev/parca/compare/ui-v0.8.3...ui-v0.9.0) (2022-02-16)
17
+
18
+ **Note:** Version bump only for package @parca/profile
19
+
20
+ ## [0.8.2](https://github.com/parca-dev/parca/compare/ui-v0.8.1...ui-v0.8.2) (2022-02-14)
21
+
22
+ # [0.8.0](https://github.com/parca-dev/parca/compare/ui-v0.7.13...ui-v0.8.0) (2022-01-31)
23
+
24
+ **Note:** Version bump only for package @parca/profile
25
+
6
26
  # [0.8.0](https://github.com/parca-dev/parca/compare/ui-v0.7.13...ui-v0.8.0) (2022-01-31)
7
27
 
8
28
  **Note:** Version bump only for package @parca/profile
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.8.0",
3
+ "version": "0.9.2",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
- "@parca/client": "^0.8.0",
7
- "@parca/dynamicsize": "^0.8.0",
8
- "@parca/parser": "^0.8.0",
6
+ "@parca/client": "^0.9.1",
7
+ "@parca/dynamicsize": "^0.9.0",
8
+ "@parca/parser": "^0.9.0",
9
9
  "d3-scale": "^4.0.2"
10
10
  },
11
11
  "main": "src/index.tsx",
@@ -19,5 +19,5 @@
19
19
  "access": "public",
20
20
  "registry": "https://registry.npmjs.org/"
21
21
  },
22
- "gitHead": "b4a7d7d958a4499fae9767c81d5b990531bd0639"
22
+ "gitHead": "544d4819ace2db4570e2780876108636860ff0d2"
23
23
  }
@@ -4,7 +4,7 @@ import {pointer} from 'd3-selection';
4
4
  import {scaleLinear} from 'd3-scale';
5
5
  import {Flamegraph, FlamegraphNode, FlamegraphRootNode} from '@parca/client';
6
6
  import {usePopper} from 'react-popper';
7
- import {valueFormatter} from '@parca/functions';
7
+ import {getLastItem, valueFormatter} from '@parca/functions';
8
8
 
9
9
  const RowHeight = 20;
10
10
 
@@ -106,13 +106,6 @@ function diffColor(diff: number, cumulative: number): string {
106
106
  return color;
107
107
  }
108
108
 
109
- function getLastItem(thePath: string): string {
110
- const index = thePath.lastIndexOf('/');
111
- if (index === -1) return thePath;
112
-
113
- return thePath.substring(index + 1);
114
- }
115
-
116
109
  export function nodeLabel(node: FlamegraphNode.AsObject): string {
117
110
  if (node.meta === undefined) return '<unknown>';
118
111
  const mapping = `${
@@ -280,6 +273,7 @@ const FlamegraphNodeTooltipTableRows = ({
280
273
  function generateGetBoundingClientRect(contextElement: Element, x = 0, y = 0) {
281
274
  const domRect = contextElement.getBoundingClientRect();
282
275
  return () =>
276
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
283
277
  ({
284
278
  width: 0,
285
279
  height: 0,
@@ -292,6 +286,7 @@ function generateGetBoundingClientRect(contextElement: Element, x = 0, y = 0) {
292
286
 
293
287
  const virtualElement = {
294
288
  getBoundingClientRect: () =>
289
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
295
290
  ({
296
291
  width: 0,
297
292
  height: 0,
@@ -0,0 +1,3 @@
1
+ .no-outline-on-buttons {
2
+ box-shadow: none !important;
3
+ }
@@ -1,17 +1,23 @@
1
- import React, {useEffect, useState, useRef} from 'react';
2
- // import ProfileSVG from './ProfileSVG'
3
- // import ProfileTop from './ProfileTop'
1
+ import React, {useEffect, useState} from 'react';
4
2
  import {CalcWidth} from '@parca/dynamicsize';
5
- import ProfileIcicleGraph from './ProfileIcicleGraph';
6
- import {ProfileSource} from './ProfileSource';
3
+ import {parseParams} from '@parca/functions';
7
4
  import {QueryRequest, QueryResponse, QueryServiceClient, ServiceError} from '@parca/client';
8
- import Card from '../../../app/web/src/components/ui/Card';
9
5
  import Button from '@parca/web/src/components/ui/Button';
10
6
  import * as parca_query_v1alpha1_query_pb from '@parca/client/src/parca/query/v1alpha1/query_pb';
11
7
 
8
+ import ProfileIcicleGraph from './ProfileIcicleGraph';
9
+ import {ProfileSource} from './ProfileSource';
10
+ import Card from '../../../app/web/src/components/ui/Card';
11
+ import TopTable from './TopTable';
12
+
13
+ import './ProfileView.styles.css';
14
+
15
+ type NavigateFunction = (path: string, queryParams: any) => void;
16
+
12
17
  interface ProfileViewProps {
13
18
  queryClient: QueryServiceClient;
14
19
  profileSource: ProfileSource;
20
+ navigateTo?: NavigateFunction;
15
21
  }
16
22
 
17
23
  export interface IQueryResult {
@@ -52,9 +58,16 @@ export const useQuery = (
52
58
  return result;
53
59
  };
54
60
 
55
- export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX.Element => {
61
+ export const ProfileView = ({
62
+ queryClient,
63
+ profileSource,
64
+ navigateTo,
65
+ }: ProfileViewProps): JSX.Element => {
66
+ const router = parseParams(window.location.search);
67
+ const currentViewFromURL = router.currentProfileView as string;
56
68
  const [curPath, setCurPath] = useState<string[]>([]);
57
69
  const {response, error} = useQuery(queryClient, profileSource);
70
+ const [currentView, setCurrentView] = useState<string | undefined>(currentViewFromURL);
58
71
 
59
72
  if (error != null) {
60
73
  return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
@@ -100,7 +113,7 @@ export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX
100
113
  e.preventDefault();
101
114
 
102
115
  const req = profileSource.QueryRequest();
103
- req.setReportType(QueryRequest.ReportType.REPORT_TYPE_PPROF_UNSPECIFIED);
116
+ req.setReportType(QueryRequest.ReportType.REPORT_TYPE_PPROF);
104
117
 
105
118
  queryClient.query(
106
119
  req,
@@ -108,6 +121,10 @@ export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX
108
121
  error: ServiceError | null,
109
122
  responseMessage: parca_query_v1alpha1_query_pb.QueryResponse | null
110
123
  ) => {
124
+ if (error != null) {
125
+ console.error('Error while querying', error);
126
+ return;
127
+ }
111
128
  if (responseMessage !== null) {
112
129
  const bytes = responseMessage.getPprof();
113
130
  const blob = new Blob([bytes], {type: 'application/octet-stream'});
@@ -116,14 +133,14 @@ export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX
116
133
  link.href = window.URL.createObjectURL(blob);
117
134
  link.download = 'profile.pb.gz';
118
135
  link.click();
136
+ } else {
137
+ console.error(error);
119
138
  }
120
139
  }
121
140
  );
122
141
  };
123
142
 
124
- const resetIcicleGraph = (e: React.MouseEvent<HTMLElement>) => {
125
- setCurPath([]);
126
- };
143
+ const resetIcicleGraph = () => setCurPath([]);
127
144
 
128
145
  const setNewCurPath = (path: string[]) => {
129
146
  if (!arrayEquals(curPath, path)) {
@@ -131,31 +148,103 @@ export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX
131
148
  }
132
149
  };
133
150
 
151
+ const switchProfileView = (view: string) => {
152
+ if (!navigateTo) return;
153
+
154
+ setCurrentView(view);
155
+
156
+ navigateTo('/', {...router, ...{currentProfileView: view}});
157
+ };
158
+
134
159
  return (
135
160
  <>
136
161
  <div className="py-3">
137
162
  <Card>
138
163
  <Card.Body>
139
- <div className="flex space-x-4 py-3">
140
- <div className="w-1/4">
141
- <Button color="neutral" onClick={resetIcicleGraph} disabled={curPath.length === 0}>
142
- Reset View
164
+ <div className="flex py-3 w-full">
165
+ <div className="w-2/5 flex space-x-4">
166
+ <div>
167
+ <Button color="neutral" onClick={downloadPProf}>
168
+ Download pprof
169
+ </Button>
170
+ </div>
171
+ </div>
172
+
173
+ <div className="flex ml-auto">
174
+ <div className="mr-3">
175
+ <Button
176
+ color="neutral"
177
+ onClick={resetIcicleGraph}
178
+ disabled={curPath.length === 0}
179
+ additionalClasses="whitespace-nowrap text-ellipsis"
180
+ >
181
+ Reset View
182
+ </Button>
183
+ </div>
184
+
185
+ <Button
186
+ color={`${currentView === 'table' ? 'primary' : 'neutral'}`}
187
+ additionalClasses={`rounded-tr-none rounded-br-none w-auto px-8 whitespace-nowrap text-ellipsis no-outline-on-buttons`}
188
+ onClick={() => switchProfileView('table')}
189
+ >
190
+ Table
191
+ </Button>
192
+
193
+ <Button
194
+ color={`${currentView === 'both' ? 'primary' : 'neutral'}`}
195
+ additionalClasses={`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
+ onClick={() => switchProfileView('both')}
197
+ >
198
+ Both
199
+ </Button>
200
+
201
+ <Button
202
+ color={`${currentView === 'icicle' ? 'primary' : 'neutral'}`}
203
+ additionalClasses={`rounded-tl-none rounded-bl-none w-auto px-8 whitespace-nowrap text-ellipsis no-outline-on-buttons`}
204
+ onClick={() => switchProfileView('icicle')}
205
+ >
206
+ Icicle Graph
143
207
  </Button>
144
208
  </div>
209
+ </div>
145
210
 
146
- <div className="w-full" />
147
- <div className="w-full" />
148
- <Button color="neutral" onClick={downloadPProf}>
149
- Download pprof
150
- </Button>
211
+ <div className="flex space-x-4">
212
+ {currentView === 'icicle' && (
213
+ <div className="w-full">
214
+ <CalcWidth throttle={300} delay={2000}>
215
+ <ProfileIcicleGraph
216
+ curPath={curPath}
217
+ setNewCurPath={setNewCurPath}
218
+ graph={response.getFlamegraph()?.toObject()}
219
+ />
220
+ </CalcWidth>
221
+ </div>
222
+ )}
223
+
224
+ {currentView === 'table' && (
225
+ <div className="w-full">
226
+ <TopTable queryClient={queryClient} profileSource={profileSource} />
227
+ </div>
228
+ )}
229
+
230
+ {currentView === 'both' && (
231
+ <>
232
+ <div className="w-1/2">
233
+ <TopTable queryClient={queryClient} profileSource={profileSource} />
234
+ </div>
235
+
236
+ <div className="w-1/2">
237
+ <CalcWidth throttle={300} delay={2000}>
238
+ <ProfileIcicleGraph
239
+ curPath={curPath}
240
+ setNewCurPath={setNewCurPath}
241
+ graph={response.getFlamegraph()?.toObject()}
242
+ />
243
+ </CalcWidth>
244
+ </div>
245
+ </>
246
+ )}
151
247
  </div>
152
- <CalcWidth throttle={300} delay={2000}>
153
- <ProfileIcicleGraph
154
- curPath={curPath}
155
- setNewCurPath={setNewCurPath}
156
- graph={response.getFlamegraph()?.toObject()}
157
- />
158
- </CalcWidth>
159
248
  </Card.Body>
160
249
  </Card>
161
250
  </div>
@@ -0,0 +1,7 @@
1
+ .iciclegraph-table .desc {
2
+ transform: rotate(0);
3
+ }
4
+
5
+ .iciclegraph-table .asc {
6
+ transform: rotate(180deg);
7
+ }
@@ -0,0 +1,188 @@
1
+ import React, {useEffect, useState} from 'react';
2
+
3
+ import {ProfileSource} from './ProfileSource';
4
+ import {QueryRequest, QueryResponse, QueryServiceClient, ServiceError} from '@parca/client';
5
+ import * as parca_query_v1alpha1_query_pb from '@parca/client/src/parca/query/v1alpha1/query_pb';
6
+ import {getLastItem, valueFormatter} from '@parca/functions';
7
+
8
+ import './TopTable.styles.css';
9
+
10
+ interface ProfileViewProps {
11
+ queryClient: QueryServiceClient;
12
+ profileSource: ProfileSource;
13
+ }
14
+
15
+ export interface IQueryResult {
16
+ response: QueryResponse | null;
17
+ error: ServiceError | null;
18
+ }
19
+
20
+ const Arrow = ({direction}: {direction: string | undefined}) => {
21
+ return (
22
+ <svg
23
+ className={`${direction !== undefined ? 'fill-[#161616] dark:fill-[#ffffff]' : ''}`}
24
+ fill="#777d87"
25
+ height="10"
26
+ viewBox="0 0 11 10"
27
+ width="11"
28
+ xmlns="http://www.w3.org/2000/svg"
29
+ >
30
+ <path clip-rule="evenodd" d="m.573997 0 5.000003 10 5-10h-9.999847z" fill-rule="evenodd" />
31
+ </svg>
32
+ );
33
+ };
34
+
35
+ const useSortableData = (
36
+ response: QueryResponse | null,
37
+ config = {key: 'cumulative', direction: 'desc'}
38
+ ) => {
39
+ const [sortConfig, setSortConfig] = React.useState<{key: string; direction: string} | null>(
40
+ config
41
+ );
42
+
43
+ const rawTableReport = response?.toObject().top?.listList;
44
+
45
+ const items = rawTableReport?.map(node => ({
46
+ ...node,
47
+ name: node.meta?.pb_function?.name,
48
+ }));
49
+
50
+ const sortedItems = React.useMemo(() => {
51
+ if (!items) return;
52
+
53
+ let sortableItems = [...items];
54
+ if (sortConfig !== null) {
55
+ sortableItems.sort((a, b) => {
56
+ if (a[sortConfig.key] < b[sortConfig.key]) {
57
+ return sortConfig.direction === 'asc' ? -1 : 1;
58
+ }
59
+ if (a[sortConfig.key] > b[sortConfig.key]) {
60
+ return sortConfig.direction === 'asc' ? 1 : -1;
61
+ }
62
+ return 0;
63
+ });
64
+ }
65
+ return sortableItems;
66
+ }, [items, sortConfig]);
67
+
68
+ const requestSort = key => {
69
+ let direction = 'desc';
70
+ if (sortConfig && sortConfig.key === key && sortConfig.direction === 'desc') {
71
+ direction = 'asc';
72
+ }
73
+ setSortConfig({key, direction});
74
+ };
75
+
76
+ return {items: sortedItems, requestSort, sortConfig};
77
+ };
78
+
79
+ export const useQuery = (
80
+ client: QueryServiceClient,
81
+ profileSource: ProfileSource
82
+ ): IQueryResult => {
83
+ const [result, setResult] = useState<IQueryResult>({
84
+ response: null,
85
+ error: null,
86
+ });
87
+
88
+ useEffect(() => {
89
+ const req = profileSource.QueryRequest();
90
+ req.setReportType(QueryRequest.ReportType.REPORT_TYPE_TOP);
91
+
92
+ client.query(
93
+ req,
94
+ (
95
+ error: ServiceError | null,
96
+ responseMessage: parca_query_v1alpha1_query_pb.QueryResponse | null
97
+ ) => {
98
+ setResult({
99
+ response: responseMessage,
100
+ error: error,
101
+ });
102
+ }
103
+ );
104
+ }, [client, profileSource]);
105
+
106
+ return result;
107
+ };
108
+
109
+ export const TopTable = ({queryClient, profileSource}: ProfileViewProps): JSX.Element => {
110
+ const {response, error} = useQuery(queryClient, profileSource);
111
+ const {items, requestSort, sortConfig} = useSortableData(response);
112
+
113
+ const unit = response?.toObject().top?.unit as string;
114
+
115
+ if (error != null) {
116
+ return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
117
+ }
118
+
119
+ const total = response?.toObject().top?.listList.length;
120
+ if (total === 0) return <>Profile has no samples</>;
121
+
122
+ const getClassNamesFor = name => {
123
+ if (!sortConfig) {
124
+ return;
125
+ }
126
+ return sortConfig.key === name ? sortConfig.direction : undefined;
127
+ };
128
+
129
+ return (
130
+ <>
131
+ <div className="w-full">
132
+ <table className="iciclegraph-table table-auto text-left w-full divide-y divide-gray-200 dark:divide-gray-700">
133
+ <thead className="bg-gray-50 dark:bg-gray-800">
134
+ <tr>
135
+ <th
136
+ className="text-sm cursor-pointer pt-2 pb-2 pl-2"
137
+ onClick={() => requestSort('name')}
138
+ >
139
+ Name
140
+ <span className={`inline-block align-middle ml-2 ${getClassNamesFor('name')}`}>
141
+ <Arrow direction={getClassNamesFor('name')} />
142
+ </span>
143
+ </th>
144
+ <th
145
+ className="text-left text-sm cursor-pointer pt-2 pb-2"
146
+ onClick={() => requestSort('flat')}
147
+ >
148
+ Flat
149
+ <span className={`inline-block align-middle ml-2 ${getClassNamesFor('flat')}`}>
150
+ <Arrow direction={getClassNamesFor('flat')} />
151
+ </span>
152
+ </th>
153
+ <th
154
+ className="text-right text-sm cursor-pointer pt-2 pb-2 pr-2"
155
+ onClick={() => requestSort('cumulative')}
156
+ >
157
+ Cumulative
158
+ <span
159
+ className={`inline-block align-middle ml-2 ${getClassNamesFor('cumulative')}`}
160
+ >
161
+ <Arrow direction={getClassNamesFor('cumulative')} />
162
+ </span>
163
+ </th>
164
+ </tr>
165
+ </thead>
166
+ <tbody className="bg-white divide-y divide-gray-200 dark:bg-gray-900 dark:divide-gray-700">
167
+ {items?.map((report, index) => (
168
+ <tr key={index} className="hover:bg-[#62626212] dark:hover:bg-[#ffffff12]">
169
+ <td className="text-xs py-1.5 pl-2 min-w-[150px] max-w-[450px]">
170
+ {report.meta?.mapping?.file !== '' && [getLastItem(report.meta?.mapping?.file)]}{' '}
171
+ {report.meta?.pb_function?.name}
172
+ </td>
173
+ <td className="text-xs min-w-[150px] max-w-[150px] py-1.5text-right">
174
+ {valueFormatter(report.flat, unit, 2)}
175
+ </td>
176
+ <td className="text-xs min-w-[150px] max-w-[150px] py-1.5 text-right pr-2">
177
+ {valueFormatter(report.cumulative, unit, 2)}
178
+ </td>
179
+ </tr>
180
+ ))}
181
+ </tbody>
182
+ </table>
183
+ </div>
184
+ </>
185
+ );
186
+ };
187
+
188
+ export default TopTable;