@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.
- package/CHANGELOG.md +40 -0
- package/dist/Callgraph/Edge/index.d.ts +22 -0
- package/dist/Callgraph/Edge/index.js +30 -0
- package/dist/Callgraph/Node/index.d.ts +19 -0
- package/dist/Callgraph/Node/index.js +37 -0
- package/dist/Callgraph/index.d.ts +8 -0
- package/dist/Callgraph/index.js +137 -0
- package/dist/Callgraph/mockData/index.d.ts +148 -0
- package/dist/Callgraph/mockData/index.js +577 -0
- package/dist/Callgraph/utils.d.ts +19 -0
- package/dist/Callgraph/utils.js +82 -0
- package/dist/GraphTooltip/index.d.ts +19 -0
- package/dist/GraphTooltip/index.js +119 -0
- package/dist/IcicleGraph.d.ts +35 -0
- package/dist/IcicleGraph.js +139 -0
- package/dist/MatchersInput/index.d.ts +23 -0
- package/dist/MatchersInput/index.js +479 -0
- package/dist/MetricsCircle/index.d.ts +7 -0
- package/dist/MetricsCircle/index.js +18 -0
- package/dist/MetricsGraph/index.d.ts +35 -0
- package/dist/MetricsGraph/index.js +349 -0
- package/dist/MetricsSeries/index.d.ts +11 -0
- package/dist/MetricsSeries/index.js +21 -0
- package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts +19 -0
- package/dist/ProfileExplorer/ProfileExplorerCompare.js +38 -0
- package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts +15 -0
- package/dist/ProfileExplorer/ProfileExplorerSingle.js +19 -0
- package/dist/ProfileExplorer/index.d.ts +9 -0
- package/dist/ProfileExplorer/index.js +203 -0
- package/dist/ProfileIcicleGraph.d.ts +10 -0
- package/dist/ProfileIcicleGraph.js +28 -0
- package/dist/ProfileMetricsGraph/index.d.ts +22 -0
- package/dist/ProfileMetricsGraph/index.js +127 -0
- package/dist/ProfileSVG.module.css +3 -0
- package/dist/ProfileSelector/CompareButton.d.ts +5 -0
- package/dist/ProfileSelector/CompareButton.js +41 -0
- package/dist/ProfileSelector/MergeButton.d.ts +5 -0
- package/dist/ProfileSelector/MergeButton.js +41 -0
- package/dist/ProfileSelector/index.d.ts +29 -0
- package/dist/ProfileSelector/index.js +133 -0
- package/dist/ProfileSource.d.ts +88 -0
- package/dist/ProfileSource.js +239 -0
- package/dist/ProfileTypeSelector/index.d.ts +20 -0
- package/dist/ProfileTypeSelector/index.js +138 -0
- package/dist/ProfileView.d.ts +39 -0
- package/dist/ProfileView.js +111 -0
- package/dist/ProfileView.styles.css +3 -0
- package/dist/ProfileViewWithData.d.ts +11 -0
- package/dist/ProfileViewWithData.js +116 -0
- package/dist/TopTable.d.ts +9 -0
- package/dist/TopTable.js +140 -0
- package/dist/TopTable.styles.css +7 -0
- package/dist/components/DiffLegend.d.ts +2 -0
- package/dist/components/DiffLegend.js +62 -0
- package/dist/components/ProfileShareButton/ResultBox.d.ts +6 -0
- package/dist/components/ProfileShareButton/ResultBox.js +46 -0
- package/dist/components/ProfileShareButton/index.d.ts +7 -0
- package/dist/components/ProfileShareButton/index.js +119 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +64 -0
- package/dist/styles.css +1 -0
- package/dist/useDelayedLoader.d.ts +5 -0
- package/dist/useDelayedLoader.js +33 -0
- package/dist/useQuery.d.ts +13 -0
- package/dist/useQuery.js +41 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +83 -0
- package/package.json +12 -8
- package/src/Callgraph/Edge/index.tsx +59 -0
- package/src/Callgraph/Node/index.tsx +66 -0
- package/src/Callgraph/index.tsx +169 -0
- package/src/Callgraph/mockData/index.ts +605 -0
- package/src/Callgraph/utils.ts +116 -0
- package/src/GraphTooltip/index.tsx +245 -0
- package/src/IcicleGraph.tsx +3 -3
- package/src/MatchersInput/index.tsx +698 -0
- package/src/MetricsCircle/index.tsx +28 -0
- package/src/MetricsGraph/index.tsx +614 -0
- package/src/MetricsSeries/index.tsx +38 -0
- package/src/ProfileExplorer/ProfileExplorerCompare.tsx +109 -0
- package/src/ProfileExplorer/ProfileExplorerSingle.tsx +72 -0
- package/src/ProfileExplorer/index.tsx +377 -0
- package/src/ProfileMetricsGraph/index.tsx +143 -0
- package/src/ProfileSelector/CompareButton.tsx +72 -0
- package/src/ProfileSelector/MergeButton.tsx +72 -0
- package/src/ProfileSelector/index.tsx +270 -0
- package/src/ProfileTypeSelector/index.tsx +180 -0
- package/src/ProfileView.tsx +2 -7
- package/src/index.tsx +11 -0
- package/src/useQuery.tsx +1 -0
- package/tailwind.config.js +8 -0
- package/tsconfig.json +7 -3
- 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;
|