@parca/profile 0.9.0 → 0.9.1

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.9.1](https://github.com/parca-dev/parca/compare/ui-v0.9.0...ui-v0.9.1) (2022-02-21)
7
+
8
+ ## [0.8.2](https://github.com/parca-dev/parca/compare/ui-v0.8.1...ui-v0.8.2) (2022-02-14)
9
+
10
+ **Note:** Version bump only for package @parca/profile
11
+
6
12
  # [0.9.0](https://github.com/parca-dev/parca/compare/ui-v0.8.3...ui-v0.9.0) (2022-02-16)
7
13
 
8
14
  **Note:** Version bump only for package @parca/profile
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
- "@parca/client": "^0.9.0",
6
+ "@parca/client": "^0.9.1",
7
7
  "@parca/dynamicsize": "^0.9.0",
8
8
  "@parca/parser": "^0.9.0",
9
9
  "d3-scale": "^4.0.2"
@@ -19,5 +19,5 @@
19
19
  "access": "public",
20
20
  "registry": "https://registry.npmjs.org/"
21
21
  },
22
- "gitHead": "a32bf468abe6c642b06635e435e8d491804e118c"
22
+ "gitHead": "3f24c77e47a520b6b2343e27621401cc2ef933c0"
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 = `${
@@ -0,0 +1,3 @@
1
+ .no-outline-on-buttons {
2
+ box-shadow: none !important;
3
+ }
@@ -1,12 +1,17 @@
1
1
  import React, {useEffect, useState} from 'react';
2
+ import {useRouter} from 'next/router';
3
+
2
4
  import {CalcWidth} from '@parca/dynamicsize';
3
5
  import ProfileIcicleGraph from './ProfileIcicleGraph';
4
6
  import {ProfileSource} from './ProfileSource';
5
7
  import {QueryRequest, QueryResponse, QueryServiceClient, ServiceError} from '@parca/client';
6
8
  import Card from '../../../app/web/src/components/ui/Card';
7
9
  import Button from '@parca/web/src/components/ui/Button';
10
+ import TopTable from './TopTable';
8
11
  import * as parca_query_v1alpha1_query_pb from '@parca/client/src/parca/query/v1alpha1/query_pb';
9
12
 
13
+ import './ProfileView.styles.css';
14
+
10
15
  interface ProfileViewProps {
11
16
  queryClient: QueryServiceClient;
12
17
  profileSource: ProfileSource;
@@ -51,8 +56,11 @@ export const useQuery = (
51
56
  };
52
57
 
53
58
  export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX.Element => {
59
+ const router = useRouter();
60
+ const currentViewFromURL = router.query.currentProfileView as string;
54
61
  const [curPath, setCurPath] = useState<string[]>([]);
55
62
  const {response, error} = useQuery(queryClient, profileSource);
63
+ const [currentView, setCurrentView] = useState<string | undefined>(currentViewFromURL);
56
64
 
57
65
  if (error != null) {
58
66
  return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
@@ -98,7 +106,7 @@ export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX
98
106
  e.preventDefault();
99
107
 
100
108
  const req = profileSource.QueryRequest();
101
- req.setReportType(QueryRequest.ReportType.REPORT_TYPE_PPROF_UNSPECIFIED);
109
+ req.setReportType(QueryRequest.ReportType.REPORT_TYPE_PPROF);
102
110
 
103
111
  queryClient.query(
104
112
  req,
@@ -133,31 +141,105 @@ export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX
133
141
  }
134
142
  };
135
143
 
144
+ const queryParams = router.query;
145
+
146
+ const switchProfileView = (view: string) => {
147
+ setCurrentView(view);
148
+ router.push({
149
+ pathname: '/',
150
+ query: {...queryParams, ...{currentProfileView: view}},
151
+ });
152
+ };
153
+
136
154
  return (
137
155
  <>
138
156
  <div className="py-3">
139
157
  <Card>
140
158
  <Card.Body>
141
- <div className="flex space-x-4 py-3">
142
- <div className="w-1/4">
143
- <Button color="neutral" onClick={resetIcicleGraph} disabled={curPath.length === 0}>
144
- Reset View
159
+ <div className="flex py-3 w-full">
160
+ <div className="w-2/5 flex space-x-4">
161
+ <div>
162
+ <Button color="neutral" onClick={downloadPProf}>
163
+ Download pprof
164
+ </Button>
165
+ </div>
166
+ </div>
167
+
168
+ <div className="flex ml-auto">
169
+ <div className="mr-3">
170
+ <Button
171
+ color="neutral"
172
+ onClick={resetIcicleGraph}
173
+ disabled={curPath.length === 0}
174
+ additionalClasses="whitespace-nowrap text-ellipsis"
175
+ >
176
+ Reset View
177
+ </Button>
178
+ </div>
179
+
180
+ <Button
181
+ color={`${currentView === 'table' ? 'primary' : 'neutral'}`}
182
+ additionalClasses={`rounded-tr-none rounded-br-none w-auto px-8 whitespace-nowrap text-ellipsis no-outline-on-buttons`}
183
+ onClick={() => switchProfileView('table')}
184
+ >
185
+ Table
186
+ </Button>
187
+
188
+ <Button
189
+ color={`${currentView === 'both' ? 'primary' : 'neutral'}`}
190
+ 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`}
191
+ onClick={() => switchProfileView('both')}
192
+ >
193
+ Both
194
+ </Button>
195
+
196
+ <Button
197
+ color={`${currentView === 'icicle' ? 'primary' : 'neutral'}`}
198
+ additionalClasses={`rounded-tl-none rounded-bl-none w-auto px-8 whitespace-nowrap text-ellipsis no-outline-on-buttons`}
199
+ onClick={() => switchProfileView('icicle')}
200
+ >
201
+ Icicle Graph
145
202
  </Button>
146
203
  </div>
204
+ </div>
147
205
 
148
- <div className="w-full" />
149
- <div className="w-full" />
150
- <Button color="neutral" onClick={downloadPProf}>
151
- Download pprof
152
- </Button>
206
+ <div className="flex space-x-4">
207
+ {currentView === 'icicle' && (
208
+ <div className="w-full">
209
+ <CalcWidth throttle={300} delay={2000}>
210
+ <ProfileIcicleGraph
211
+ curPath={curPath}
212
+ setNewCurPath={setNewCurPath}
213
+ graph={response.getFlamegraph()?.toObject()}
214
+ />
215
+ </CalcWidth>
216
+ </div>
217
+ )}
218
+
219
+ {currentView === 'table' && (
220
+ <div className="w-full">
221
+ <TopTable queryClient={queryClient} profileSource={profileSource} />
222
+ </div>
223
+ )}
224
+
225
+ {currentView === 'both' && (
226
+ <>
227
+ <div className="w-1/2">
228
+ <TopTable queryClient={queryClient} profileSource={profileSource} />
229
+ </div>
230
+
231
+ <div className="w-1/2">
232
+ <CalcWidth throttle={300} delay={2000}>
233
+ <ProfileIcicleGraph
234
+ curPath={curPath}
235
+ setNewCurPath={setNewCurPath}
236
+ graph={response.getFlamegraph()?.toObject()}
237
+ />
238
+ </CalcWidth>
239
+ </div>
240
+ </>
241
+ )}
153
242
  </div>
154
- <CalcWidth throttle={300} delay={2000}>
155
- <ProfileIcicleGraph
156
- curPath={curPath}
157
- setNewCurPath={setNewCurPath}
158
- graph={response.getFlamegraph()?.toObject()}
159
- />
160
- </CalcWidth>
161
243
  </Card.Body>
162
244
  </Card>
163
245
  </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;