@parca/profile 0.16.0 → 0.16.22

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 (93) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/Callgraph/Edge/index.d.ts +22 -0
  3. package/dist/Callgraph/Edge/index.js +30 -0
  4. package/dist/Callgraph/Node/index.d.ts +19 -0
  5. package/dist/Callgraph/Node/index.js +37 -0
  6. package/dist/Callgraph/index.d.ts +8 -0
  7. package/dist/Callgraph/index.js +137 -0
  8. package/dist/Callgraph/mockData/index.d.ts +148 -0
  9. package/dist/Callgraph/mockData/index.js +577 -0
  10. package/dist/Callgraph/utils.d.ts +19 -0
  11. package/dist/Callgraph/utils.js +82 -0
  12. package/dist/GraphTooltip/index.d.ts +19 -0
  13. package/dist/GraphTooltip/index.js +119 -0
  14. package/dist/IcicleGraph.d.ts +35 -0
  15. package/dist/IcicleGraph.js +139 -0
  16. package/dist/MatchersInput/index.d.ts +23 -0
  17. package/dist/MatchersInput/index.js +479 -0
  18. package/dist/MetricsCircle/index.d.ts +7 -0
  19. package/dist/MetricsCircle/index.js +18 -0
  20. package/dist/MetricsGraph/index.d.ts +35 -0
  21. package/dist/MetricsGraph/index.js +349 -0
  22. package/dist/MetricsSeries/index.d.ts +11 -0
  23. package/dist/MetricsSeries/index.js +21 -0
  24. package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts +19 -0
  25. package/dist/ProfileExplorer/ProfileExplorerCompare.js +38 -0
  26. package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts +15 -0
  27. package/dist/ProfileExplorer/ProfileExplorerSingle.js +19 -0
  28. package/dist/ProfileExplorer/index.d.ts +9 -0
  29. package/dist/ProfileExplorer/index.js +203 -0
  30. package/dist/ProfileIcicleGraph.d.ts +10 -0
  31. package/dist/ProfileIcicleGraph.js +28 -0
  32. package/dist/ProfileMetricsGraph/index.d.ts +22 -0
  33. package/dist/ProfileMetricsGraph/index.js +127 -0
  34. package/dist/ProfileSVG.module.css +3 -0
  35. package/dist/ProfileSelector/CompareButton.d.ts +5 -0
  36. package/dist/ProfileSelector/CompareButton.js +41 -0
  37. package/dist/ProfileSelector/MergeButton.d.ts +5 -0
  38. package/dist/ProfileSelector/MergeButton.js +41 -0
  39. package/dist/ProfileSelector/index.d.ts +29 -0
  40. package/dist/ProfileSelector/index.js +133 -0
  41. package/dist/ProfileSource.d.ts +88 -0
  42. package/dist/ProfileSource.js +239 -0
  43. package/dist/ProfileTypeSelector/index.d.ts +20 -0
  44. package/dist/ProfileTypeSelector/index.js +138 -0
  45. package/dist/ProfileView.d.ts +39 -0
  46. package/dist/ProfileView.js +111 -0
  47. package/dist/ProfileView.styles.css +3 -0
  48. package/dist/ProfileViewWithData.d.ts +11 -0
  49. package/dist/ProfileViewWithData.js +116 -0
  50. package/dist/TopTable.d.ts +9 -0
  51. package/dist/TopTable.js +140 -0
  52. package/dist/TopTable.styles.css +7 -0
  53. package/dist/components/DiffLegend.d.ts +2 -0
  54. package/dist/components/DiffLegend.js +62 -0
  55. package/dist/components/ProfileShareButton/ResultBox.d.ts +6 -0
  56. package/dist/components/ProfileShareButton/ResultBox.js +46 -0
  57. package/dist/components/ProfileShareButton/index.d.ts +7 -0
  58. package/dist/components/ProfileShareButton/index.js +119 -0
  59. package/dist/index.d.ts +13 -0
  60. package/dist/index.js +64 -0
  61. package/dist/styles.css +1 -0
  62. package/dist/useDelayedLoader.d.ts +5 -0
  63. package/dist/useDelayedLoader.js +33 -0
  64. package/dist/useQuery.d.ts +13 -0
  65. package/dist/useQuery.js +41 -0
  66. package/dist/utils.d.ts +4 -0
  67. package/dist/utils.js +83 -0
  68. package/package.json +12 -8
  69. package/src/Callgraph/Edge/index.tsx +59 -0
  70. package/src/Callgraph/Node/index.tsx +66 -0
  71. package/src/Callgraph/index.tsx +169 -0
  72. package/src/Callgraph/mockData/index.ts +605 -0
  73. package/src/Callgraph/utils.ts +116 -0
  74. package/src/GraphTooltip/index.tsx +245 -0
  75. package/src/IcicleGraph.tsx +3 -3
  76. package/src/MatchersInput/index.tsx +698 -0
  77. package/src/MetricsCircle/index.tsx +28 -0
  78. package/src/MetricsGraph/index.tsx +614 -0
  79. package/src/MetricsSeries/index.tsx +38 -0
  80. package/src/ProfileExplorer/ProfileExplorerCompare.tsx +109 -0
  81. package/src/ProfileExplorer/ProfileExplorerSingle.tsx +72 -0
  82. package/src/ProfileExplorer/index.tsx +377 -0
  83. package/src/ProfileMetricsGraph/index.tsx +143 -0
  84. package/src/ProfileSelector/CompareButton.tsx +72 -0
  85. package/src/ProfileSelector/MergeButton.tsx +72 -0
  86. package/src/ProfileSelector/index.tsx +270 -0
  87. package/src/ProfileTypeSelector/index.tsx +180 -0
  88. package/src/ProfileView.tsx +2 -7
  89. package/src/index.tsx +11 -0
  90. package/src/useQuery.tsx +1 -0
  91. package/tailwind.config.js +8 -0
  92. package/tsconfig.json +7 -3
  93. package/typings.d.ts +14 -0
@@ -0,0 +1,109 @@
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 {ProfileDiffSource, ProfileSelection, ProfileViewWithData} from '..';
15
+ import {Query} from '@parca/parser';
16
+ import {QueryServiceClient} from '@parca/client';
17
+
18
+ import {NavigateFunction} from '.';
19
+ import ProfileSelector, {QuerySelection} from '../ProfileSelector';
20
+
21
+ interface ProfileExplorerCompareProps {
22
+ queryClient: QueryServiceClient;
23
+
24
+ queryA: QuerySelection;
25
+ queryB: QuerySelection;
26
+ profileA: ProfileSelection | null;
27
+ profileB: ProfileSelection | null;
28
+ selectQueryA: (query: QuerySelection) => void;
29
+ selectQueryB: (query: QuerySelection) => void;
30
+ selectProfileA: (source: ProfileSelection) => void;
31
+ selectProfileB: (source: ProfileSelection) => void;
32
+ closeProfile: (card: string) => void;
33
+
34
+ navigateTo: NavigateFunction;
35
+ }
36
+
37
+ const ProfileExplorerCompare = ({
38
+ queryClient,
39
+ queryA,
40
+ queryB,
41
+ profileA,
42
+ profileB,
43
+ selectQueryA,
44
+ selectQueryB,
45
+ selectProfileA,
46
+ selectProfileB,
47
+ closeProfile,
48
+ navigateTo,
49
+ }: ProfileExplorerCompareProps): JSX.Element => {
50
+ const closeProfileA = (): void => {
51
+ closeProfile('A');
52
+ };
53
+
54
+ const closeProfileB = (): void => {
55
+ closeProfile('B');
56
+ };
57
+
58
+ return (
59
+ <>
60
+ <div className="grid grid-cols-2">
61
+ <div className="pr-2">
62
+ <ProfileSelector
63
+ queryClient={queryClient}
64
+ querySelection={queryA}
65
+ profileSelection={profileA}
66
+ selectProfile={selectProfileA}
67
+ selectQuery={selectQueryA}
68
+ closeProfile={closeProfileA}
69
+ enforcedProfileName={''}
70
+ comparing={true}
71
+ onCompareProfile={() => {}}
72
+ />
73
+ </div>
74
+ <div className="pl-2">
75
+ <ProfileSelector
76
+ queryClient={queryClient}
77
+ querySelection={queryB}
78
+ profileSelection={profileB}
79
+ selectProfile={selectProfileB}
80
+ selectQuery={selectQueryB}
81
+ closeProfile={closeProfileB}
82
+ enforcedProfileName={Query.parse(queryA.expression).profileName()}
83
+ comparing={true}
84
+ onCompareProfile={() => {}}
85
+ />
86
+ </div>
87
+ </div>
88
+ <div className="grid grid-cols-1">
89
+ {profileA != null && profileB != null ? (
90
+ <ProfileViewWithData
91
+ navigateTo={navigateTo}
92
+ queryClient={queryClient}
93
+ profileSource={
94
+ new ProfileDiffSource(profileA.ProfileSource(), profileB.ProfileSource())
95
+ }
96
+ />
97
+ ) : (
98
+ <div>
99
+ <div className="my-20 text-center">
100
+ <p>Select a profile on both sides.</p>
101
+ </div>
102
+ </div>
103
+ )}
104
+ </div>
105
+ </>
106
+ );
107
+ };
108
+
109
+ export default ProfileExplorerCompare;
@@ -0,0 +1,72 @@
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 {QueryServiceClient} from '@parca/client';
15
+ import {ProfileSelection, ProfileViewWithData} from '..';
16
+
17
+ import {NavigateFunction} from '../ProfileExplorer';
18
+ import ProfileSelector, {QuerySelection} from '../ProfileSelector';
19
+ interface ProfileExplorerSingleProps {
20
+ queryClient: QueryServiceClient;
21
+ query: QuerySelection;
22
+ selectQuery: (query: QuerySelection) => void;
23
+ selectProfile: (source: ProfileSelection) => void;
24
+ profile: ProfileSelection | null;
25
+ compareProfile: () => void;
26
+ navigateTo: NavigateFunction;
27
+ }
28
+
29
+ const ProfileExplorerSingle = ({
30
+ queryClient,
31
+ query,
32
+ selectQuery,
33
+ selectProfile,
34
+ profile,
35
+ compareProfile,
36
+ navigateTo,
37
+ }: ProfileExplorerSingleProps): JSX.Element => {
38
+ return (
39
+ <>
40
+ <div className="grid grid-cols-1">
41
+ <div>
42
+ <ProfileSelector
43
+ queryClient={queryClient}
44
+ querySelection={query}
45
+ selectQuery={selectQuery}
46
+ selectProfile={selectProfile}
47
+ closeProfile={() => {}}
48
+ profileSelection={profile}
49
+ comparing={false}
50
+ onCompareProfile={compareProfile}
51
+ enforcedProfileName={''} // TODO
52
+ />
53
+ </div>
54
+ </div>
55
+ <div className="grid grid-cols-1">
56
+ <div>
57
+ {profile != null ? (
58
+ <ProfileViewWithData
59
+ queryClient={queryClient}
60
+ profileSource={profile.ProfileSource()}
61
+ navigateTo={navigateTo}
62
+ />
63
+ ) : (
64
+ <></>
65
+ )}
66
+ </div>
67
+ </div>
68
+ </>
69
+ );
70
+ };
71
+
72
+ export default ProfileExplorerSingle;
@@ -0,0 +1,377 @@
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 {QuerySelection} from '../ProfileSelector';
15
+ import {ProfileSelection, ProfileSelectionFromParams, SuffixParams} from '..';
16
+ import ProfileExplorerSingle from './ProfileExplorerSingle';
17
+ import ProfileExplorerCompare from './ProfileExplorerCompare';
18
+ import {QueryServiceClient} from '@parca/client';
19
+ import {
20
+ useAppSelector,
21
+ useAppDispatch,
22
+ setCompare,
23
+ selectCompareMode,
24
+ setSearchNodeString,
25
+ store,
26
+ } from '@parca/store';
27
+ import {Provider, batch} from 'react-redux';
28
+ import {DateTimeRange} from '@parca/components';
29
+ import {useEffect} from 'react';
30
+
31
+ export type NavigateFunction = (path: string, queryParams: any) => void;
32
+
33
+ interface ProfileExplorerProps {
34
+ queryClient: QueryServiceClient;
35
+ queryParams: any;
36
+ navigateTo: NavigateFunction;
37
+ }
38
+
39
+ const getExpressionAsAString = (expression: string | []): string => {
40
+ const x = Array.isArray(expression) ? expression.join() : expression;
41
+ return x;
42
+ };
43
+
44
+ /* eslint-disable @typescript-eslint/naming-convention */
45
+ const sanitizeDateRange = (
46
+ time_selection_a: string,
47
+ from_a: number,
48
+ to_a: number
49
+ ): {time_selection_a: string; from_a: number; to_a: number} => {
50
+ const range = DateTimeRange.fromRangeKey(time_selection_a);
51
+ if (from_a == null && to_a == null) {
52
+ from_a = range.getFromMs();
53
+ to_a = range.getToMs();
54
+ }
55
+ return {time_selection_a: range.getRangeKey(), from_a, to_a};
56
+ };
57
+ /* eslint-enable @typescript-eslint/naming-convention */
58
+
59
+ const ProfileExplorerApp = ({
60
+ queryClient,
61
+ queryParams,
62
+ navigateTo,
63
+ }: ProfileExplorerProps): JSX.Element => {
64
+ const dispatch = useAppDispatch();
65
+ const compareMode = useAppSelector(selectCompareMode);
66
+
67
+ /* eslint-disable @typescript-eslint/naming-convention */
68
+ let {
69
+ from_a,
70
+ to_a,
71
+ merge_a,
72
+ profile_name_a,
73
+ labels_a,
74
+ time_a,
75
+ time_selection_a,
76
+ compare_a,
77
+ from_b,
78
+ to_b,
79
+ merge_b,
80
+ profile_name_b,
81
+ labels_b,
82
+ time_b,
83
+ time_selection_b,
84
+ compare_b,
85
+ } = queryParams;
86
+ /* eslint-enable @typescript-eslint/naming-convention */
87
+
88
+ const sanitizedRange = sanitizeDateRange(time_selection_a, from_a, to_a);
89
+ time_selection_a = sanitizedRange.time_selection_a;
90
+ from_a = sanitizedRange.from_a;
91
+ to_a = sanitizedRange.to_a;
92
+
93
+ // eslint-disable-next-line @typescript-eslint/naming-convention
94
+ const expression_a = getExpressionAsAString(queryParams.expression_a);
95
+
96
+ // eslint-disable-next-line @typescript-eslint/naming-convention
97
+ const expression_b = getExpressionAsAString(queryParams.expression_b);
98
+
99
+ if ((queryParams?.expression_a ?? '') !== '') queryParams.expression_a = expression_a;
100
+ if ((queryParams?.expression_b ?? '') !== '') queryParams.expression_b = expression_b;
101
+
102
+ useEffect(() => {
103
+ if (compare_a === 'true' && compare_b === 'true') {
104
+ dispatch(setCompare(true));
105
+ } else {
106
+ dispatch(setCompare(false));
107
+ }
108
+ }, [dispatch, compare_a, compare_b]);
109
+
110
+ const filterSuffix = (
111
+ o: {[key: string]: string | string[] | undefined},
112
+ suffix: string
113
+ ): {[key: string]: string | string[] | undefined} =>
114
+ Object.fromEntries(Object.entries(o).filter(([key]) => !key.endsWith(suffix)));
115
+
116
+ const swapQueryParameters = (o: {
117
+ [key: string]: string | string[] | undefined;
118
+ }): {[key: string]: string | string[] | undefined} => {
119
+ Object.entries(o).forEach(([key, value]) => {
120
+ if (key.endsWith('_b')) {
121
+ o[key.slice(0, -2) + '_a'] = value;
122
+ }
123
+ });
124
+ return o;
125
+ };
126
+
127
+ const selectProfileA = (p: ProfileSelection): void => {
128
+ queryParams.expression_a = encodeURIComponent(queryParams.expression_a);
129
+ queryParams.expression_b = encodeURIComponent(queryParams.expression_b);
130
+ return navigateTo('/', {
131
+ ...queryParams,
132
+ ...SuffixParams(p.HistoryParams(), '_a'),
133
+ });
134
+ };
135
+
136
+ const selectProfileB = (p: ProfileSelection): void => {
137
+ queryParams.expression_a = encodeURIComponent(queryParams.expression_a);
138
+ queryParams.expression_b = encodeURIComponent(queryParams.expression_b);
139
+ return navigateTo('/', {
140
+ ...queryParams,
141
+ ...SuffixParams(p.HistoryParams(), '_b'),
142
+ });
143
+ };
144
+
145
+ // Show the SingleProfileExplorer when not comparing
146
+ if (compare_a !== 'true' && compare_b !== 'true') {
147
+ const query = {
148
+ expression: expression_a,
149
+ from: parseInt(from_a as string),
150
+ to: parseInt(to_a as string),
151
+ merge: (merge_a as string) === 'true',
152
+ profile_name: profile_name_a as string,
153
+ timeSelection: time_selection_a as string,
154
+ };
155
+
156
+ const profile = ProfileSelectionFromParams(
157
+ expression_a,
158
+ from_a as string,
159
+ to_a as string,
160
+ merge_a as string,
161
+ labels_a as string[],
162
+ profile_name_a as string,
163
+ time_a as string
164
+ );
165
+
166
+ const selectQuery = (q: QuerySelection): void => {
167
+ return navigateTo(
168
+ '/',
169
+ // Filtering the _a suffix causes us to reset potential profile
170
+ // selection when running a new query.
171
+ {
172
+ ...filterSuffix(queryParams, '_a'),
173
+ ...{
174
+ expression_a: encodeURIComponent(q.expression),
175
+ from_a: q.from.toString(),
176
+ to_a: q.to.toString(),
177
+ merge_a: q.merge,
178
+ time_selection_a: q.timeSelection,
179
+ currentProfileView: 'icicle',
180
+ },
181
+ }
182
+ );
183
+ };
184
+
185
+ const selectProfile = (p: ProfileSelection): void => {
186
+ queryParams.expression_a = encodeURIComponent(queryParams.expression_a);
187
+ return navigateTo('/', {
188
+ ...queryParams,
189
+ ...SuffixParams(p.HistoryParams(), '_a'),
190
+ });
191
+ };
192
+
193
+ const compareProfile = (): void => {
194
+ let compareQuery = {
195
+ compare_a: 'true',
196
+ expression_a: encodeURIComponent(query.expression),
197
+ from_a: query.from.toString(),
198
+ to_a: query.to.toString(),
199
+ merge_a: query.merge,
200
+ time_selection_a: query.timeSelection,
201
+ profile_name_a: query.profile_name,
202
+
203
+ compare_b: 'true',
204
+ expression_b: encodeURIComponent(query.expression),
205
+ from_b: query.from.toString(),
206
+ to_b: query.to.toString(),
207
+ merge_b: query.merge,
208
+ time_selection_b: query.timeSelection,
209
+ profile_name_b: query.profile_name,
210
+ };
211
+
212
+ if (profile != null) {
213
+ compareQuery = {
214
+ ...SuffixParams(profile.HistoryParams(), '_a'),
215
+ ...compareQuery,
216
+ };
217
+ }
218
+
219
+ compareQuery = {
220
+ ...compareQuery,
221
+ ...{
222
+ currentProfileView: 'icicle',
223
+ },
224
+ };
225
+
226
+ batch(() => {
227
+ dispatch(setCompare(!compareMode));
228
+ dispatch(setSearchNodeString(undefined));
229
+ });
230
+
231
+ void navigateTo('/', compareQuery);
232
+ };
233
+
234
+ return (
235
+ <ProfileExplorerSingle
236
+ queryClient={queryClient}
237
+ query={query}
238
+ profile={profile}
239
+ selectQuery={selectQuery}
240
+ selectProfile={selectProfile}
241
+ compareProfile={compareProfile}
242
+ navigateTo={navigateTo}
243
+ />
244
+ );
245
+ }
246
+
247
+ const queryA = {
248
+ expression: expression_a,
249
+ from: parseInt(from_a as string),
250
+ to: parseInt(to_a as string),
251
+ merge: (merge_a as string) === 'true',
252
+ timeSelection: time_selection_a as string,
253
+ profile_name: profile_name_a as string,
254
+ };
255
+ const queryB = {
256
+ expression: expression_b,
257
+ from: parseInt(from_b as string),
258
+ to: parseInt(to_b as string),
259
+ merge: (merge_b as string) === 'true',
260
+ timeSelection: time_selection_b as string,
261
+ profile_name: profile_name_b as string,
262
+ };
263
+
264
+ const profileA = ProfileSelectionFromParams(
265
+ expression_a,
266
+ from_a as string,
267
+ to_a as string,
268
+ merge_a as string,
269
+ labels_a as string[],
270
+ profile_name_a as string,
271
+ time_a as string
272
+ );
273
+ const profileB = ProfileSelectionFromParams(
274
+ expression_b,
275
+ from_b as string,
276
+ to_b as string,
277
+ merge_b as string,
278
+ labels_b as string[],
279
+ profile_name_b as string,
280
+ time_b as string
281
+ );
282
+
283
+ const selectQueryA = (q: QuerySelection): void => {
284
+ return navigateTo(
285
+ '/',
286
+ // Filtering the _a suffix causes us to reset potential profile
287
+ // selection when running a new query.
288
+ {
289
+ ...filterSuffix(queryParams, '_a'),
290
+ ...{
291
+ compare_a: 'true',
292
+ expression_a: encodeURIComponent(q.expression),
293
+ expression_b: encodeURIComponent(expression_b),
294
+ from_a: q.from.toString(),
295
+ to_a: q.to.toString(),
296
+ merge_a: q.merge,
297
+ time_selection_a: q.timeSelection,
298
+ },
299
+ }
300
+ );
301
+ };
302
+
303
+ const selectQueryB = (q: QuerySelection): void => {
304
+ return navigateTo(
305
+ '/',
306
+ // Filtering the _b suffix causes us to reset potential profile
307
+ // selection when running a new query.
308
+ {
309
+ ...filterSuffix(queryParams, '_b'),
310
+ ...{
311
+ compare_b: 'true',
312
+ expression_b: encodeURIComponent(q.expression),
313
+ expression_a: encodeURIComponent(expression_a),
314
+ from_b: q.from.toString(),
315
+ to_b: q.to.toString(),
316
+ merge_b: q.merge,
317
+ time_selection_b: q.timeSelection,
318
+ },
319
+ }
320
+ );
321
+ };
322
+
323
+ const closeProfile = (card: string): void => {
324
+ let newQueryParameters = queryParams;
325
+ if (card === 'A') {
326
+ newQueryParameters = swapQueryParameters(queryParams);
327
+ }
328
+
329
+ batch(() => {
330
+ dispatch(setCompare(!compareMode));
331
+ dispatch(setSearchNodeString(undefined));
332
+ });
333
+
334
+ return navigateTo('/', {
335
+ ...filterSuffix(newQueryParameters, '_b'),
336
+ ...{
337
+ compare_a: 'false',
338
+ },
339
+ });
340
+ };
341
+
342
+ return (
343
+ <ProfileExplorerCompare
344
+ queryClient={queryClient}
345
+ queryA={queryA}
346
+ queryB={queryB}
347
+ profileA={profileA}
348
+ profileB={profileB}
349
+ selectQueryA={selectQueryA}
350
+ selectQueryB={selectQueryB}
351
+ selectProfileA={selectProfileA}
352
+ selectProfileB={selectProfileB}
353
+ closeProfile={closeProfile}
354
+ navigateTo={navigateTo}
355
+ />
356
+ );
357
+ };
358
+
359
+ const ProfileExplorer = ({
360
+ queryClient,
361
+ queryParams,
362
+ navigateTo,
363
+ }: ProfileExplorerProps): JSX.Element => {
364
+ const {store: reduxStore} = store();
365
+
366
+ return (
367
+ <Provider store={reduxStore}>
368
+ <ProfileExplorerApp
369
+ queryClient={queryClient}
370
+ queryParams={queryParams}
371
+ navigateTo={navigateTo}
372
+ />
373
+ </Provider>
374
+ );
375
+ };
376
+
377
+ export default ProfileExplorer;
@@ -0,0 +1,143 @@
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 {useState, useEffect} from 'react';
15
+ import MetricsGraph from '../MetricsGraph';
16
+ import {ProfileSelection, SingleProfileSelection} from '..';
17
+ import {QueryServiceClient, QueryRangeResponse, Label, Timestamp} from '@parca/client';
18
+ import {RpcError} from '@protobuf-ts/runtime-rpc';
19
+ import {DateTimeRange, useGrpcMetadata, useParcaTheme} from '@parca/components';
20
+ import {Query} from '@parca/parser';
21
+ import useDelayedLoader from '../useDelayedLoader';
22
+
23
+ interface ProfileMetricsGraphProps {
24
+ queryClient: QueryServiceClient;
25
+ queryExpression: string;
26
+ profile: ProfileSelection | null;
27
+ from: number;
28
+ to: number;
29
+ select: (source: ProfileSelection) => void;
30
+ setTimeRange: (range: DateTimeRange) => void;
31
+ addLabelMatcher: (key: string, value: string) => void;
32
+ }
33
+
34
+ export interface IQueryRangeResult {
35
+ response: QueryRangeResponse | null;
36
+ isLoading: boolean;
37
+ error: RpcError | null;
38
+ }
39
+
40
+ export const useQueryRange = (
41
+ client: QueryServiceClient,
42
+ queryExpression: string,
43
+ start: number,
44
+ end: number
45
+ ): IQueryRangeResult => {
46
+ const [isLoading, setIsLoading] = useState<boolean>(false);
47
+ const [error, setError] = useState<any>(null);
48
+ const [response, setResponse] = useState<QueryRangeResponse | null>(null);
49
+ const metadata = useGrpcMetadata();
50
+
51
+ useEffect(() => {
52
+ void (async () => {
53
+ setIsLoading(true);
54
+
55
+ try {
56
+ const {response} = await client.queryRange(
57
+ {
58
+ query: queryExpression,
59
+ start: Timestamp.fromDate(new Date(start)),
60
+ end: Timestamp.fromDate(new Date(end)),
61
+ limit: 0,
62
+ },
63
+ {meta: metadata}
64
+ );
65
+ setResponse(response);
66
+ } catch (e) {
67
+ setError(e);
68
+ } finally {
69
+ setIsLoading(false);
70
+ }
71
+ })();
72
+ }, [client, queryExpression, start, end, metadata]);
73
+
74
+ return {isLoading, error, response};
75
+ };
76
+
77
+ const ProfileMetricsGraph = ({
78
+ queryClient,
79
+ queryExpression,
80
+ profile,
81
+ from,
82
+ to,
83
+ select,
84
+ setTimeRange,
85
+ addLabelMatcher,
86
+ }: ProfileMetricsGraphProps): JSX.Element => {
87
+ const {isLoading, response, error} = useQueryRange(queryClient, queryExpression, from, to);
88
+ const isLoaderVisible = useDelayedLoader(isLoading);
89
+ const {loader} = useParcaTheme();
90
+
91
+ if (isLoaderVisible) {
92
+ return <>{loader}</>;
93
+ }
94
+
95
+ if (error !== null) {
96
+ return (
97
+ <div
98
+ className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
99
+ role="alert"
100
+ >
101
+ <strong className="font-bold">Error! </strong>
102
+ <span className="block sm:inline">{error.message}</span>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ const series = response?.series;
108
+ if (series !== null && series !== undefined && series?.length > 0) {
109
+ const handleSampleClick = (timestamp: number, _value: number, labels: Label[]): void => {
110
+ select(
111
+ new SingleProfileSelection(Query.parse(queryExpression).profileName(), labels, timestamp)
112
+ );
113
+ };
114
+
115
+ return (
116
+ <div
117
+ className="dark:bg-gray-700 rounded border-gray-300 dark:border-gray-500"
118
+ style={{borderWidth: 1}}
119
+ >
120
+ <MetricsGraph
121
+ data={series}
122
+ from={from}
123
+ to={to}
124
+ profile={profile as SingleProfileSelection}
125
+ setTimeRange={setTimeRange}
126
+ onSampleClick={handleSampleClick}
127
+ onLabelClick={addLabelMatcher}
128
+ width={0}
129
+ sampleUnit={Query.parse(queryExpression).profileType().sampleUnit}
130
+ />
131
+ </div>
132
+ );
133
+ }
134
+ return (
135
+ <div className="grid grid-cols-1">
136
+ <div className="py-20 flex justify-center">
137
+ <p className="m-0">No data found. Try a different query.</p>
138
+ </div>
139
+ </div>
140
+ );
141
+ };
142
+
143
+ export default ProfileMetricsGraph;