@parca/profile 0.16.198 → 0.16.200
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 +8 -0
- package/dist/Callgraph/index.d.ts +0 -1
- package/dist/GraphTooltip/ExpandOnHoverValue.d.ts +0 -1
- package/dist/GraphTooltip/index.d.ts +0 -1
- package/dist/GraphTooltipArrow/ExpandOnHoverValue.d.ts +6 -0
- package/dist/GraphTooltipArrow/ExpandOnHoverValue.js +4 -0
- package/dist/GraphTooltipArrow/index.d.ts +43 -0
- package/dist/GraphTooltipArrow/index.js +244 -0
- package/dist/MatchersInput/SuggestionItem.d.ts +0 -1
- package/dist/MatchersInput/SuggestionsList.d.ts +0 -1
- package/dist/MatchersInput/index.d.ts +0 -1
- package/dist/MetricsCircle/index.d.ts +0 -1
- package/dist/MetricsGraph/MetricsTooltip/index.d.ts +0 -1
- package/dist/MetricsGraph/index.d.ts +5 -3
- package/dist/MetricsGraph/index.js +3 -8
- package/dist/MetricsGraph/useMetricsGraphDimensions.d.ts +9 -0
- package/dist/MetricsGraph/useMetricsGraphDimensions.js +29 -0
- package/dist/MetricsSeries/index.d.ts +0 -1
- package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts +0 -1
- package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts +0 -1
- package/dist/ProfileExplorer/index.d.ts +0 -1
- package/dist/ProfileIcicleGraph/IcicleGraph/ColorStackLegend.d.ts +0 -1
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/ColorStackLegend.d.ts +10 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/ColorStackLegend.js +64 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.d.ts +48 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.js +150 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.d.ts +23 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.js +110 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/useNodeColor.d.ts +14 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/useNodeColor.js +26 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/utils.d.ts +4 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/utils.js +42 -0
- package/dist/ProfileIcicleGraph/index.d.ts +4 -3
- package/dist/ProfileIcicleGraph/index.js +7 -5
- package/dist/ProfileMetricsGraph/index.d.ts +4 -1
- package/dist/ProfileMetricsGraph/index.js +24 -14
- package/dist/ProfileSelector/CompareButton.d.ts +0 -1
- package/dist/ProfileSelector/index.d.ts +0 -1
- package/dist/ProfileSelector/index.js +44 -35
- package/dist/ProfileSelector/useAutoQuerySelector.d.ts +9 -0
- package/dist/ProfileSelector/useAutoQuerySelector.js +66 -0
- package/dist/ProfileSource.d.ts +0 -1
- package/dist/ProfileTypeSelector/index.d.ts +1 -1
- package/dist/ProfileTypeSelector/index.js +4 -3
- package/dist/ProfileView/FilterByFunctionButton.d.ts +0 -1
- package/dist/ProfileView/ViewSelector.d.ts +0 -1
- package/dist/ProfileView/index.d.ts +2 -1
- package/dist/ProfileView/index.js +3 -3
- package/dist/ProfileViewWithData.d.ts +0 -1
- package/dist/ProfileViewWithData.js +16 -7
- package/dist/components/DiffLegend.d.ts +0 -1
- package/dist/components/ProfileShareButton/ResultBox.d.ts +0 -1
- package/dist/components/ProfileShareButton/index.d.ts +0 -1
- package/dist/styles.css +1 -1
- package/package.json +6 -5
- package/src/GraphTooltipArrow/ExpandOnHoverValue.tsx +30 -0
- package/src/GraphTooltipArrow/index.tsx +564 -0
- package/src/MetricsGraph/index.tsx +24 -20
- package/src/MetricsGraph/useMetricsGraphDimensions.ts +42 -0
- package/src/ProfileIcicleGraph/IcicleGraphArrow/ColorStackLegend.tsx +109 -0
- package/src/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.tsx +327 -0
- package/src/ProfileIcicleGraph/IcicleGraphArrow/index.tsx +212 -0
- package/src/ProfileIcicleGraph/IcicleGraphArrow/useNodeColor.ts +52 -0
- package/src/ProfileIcicleGraph/IcicleGraphArrow/utils.ts +51 -0
- package/src/ProfileIcicleGraph/index.tsx +34 -14
- package/src/ProfileMetricsGraph/index.tsx +38 -20
- package/src/ProfileSelector/index.tsx +66 -54
- package/src/ProfileSelector/useAutoQuerySelector.ts +87 -0
- package/src/ProfileTypeSelector/index.tsx +9 -10
- package/src/ProfileView/index.tsx +5 -2
- package/src/ProfileViewWithData.tsx +22 -7
|
@@ -0,0 +1,51 @@
|
|
|
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 {Table} from 'apache-arrow';
|
|
15
|
+
|
|
16
|
+
import {EVERYTHING_ELSE, FEATURE_TYPES, type Feature} from '@parca/store';
|
|
17
|
+
import {getLastItem} from '@parca/utilities';
|
|
18
|
+
|
|
19
|
+
import {hexifyAddress} from '../../utils';
|
|
20
|
+
import {FIELD_FUNCTION_NAME, FIELD_LOCATION_ADDRESS, FIELD_MAPPING_FILE} from './index';
|
|
21
|
+
|
|
22
|
+
export function nodeLabel(table: Table<any>, row: number, showBinaryName: boolean): string {
|
|
23
|
+
const functionName: string | null = table.getChild(FIELD_FUNCTION_NAME)?.get(row);
|
|
24
|
+
if (functionName !== null && functionName !== '') {
|
|
25
|
+
return functionName;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let mappingString = '';
|
|
29
|
+
if (showBinaryName) {
|
|
30
|
+
const mappingFile: string | null = table.getChild(FIELD_MAPPING_FILE)?.get(row) ?? '';
|
|
31
|
+
const binary: string | undefined = getLastItem(mappingFile ?? undefined);
|
|
32
|
+
if (binary != null) mappingString = `[${binary}]`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const addressBigInt: bigint = table.getChild(FIELD_LOCATION_ADDRESS)?.get(row);
|
|
36
|
+
const address = hexifyAddress(addressBigInt);
|
|
37
|
+
const fallback = `${mappingString}${address}`;
|
|
38
|
+
return fallback === '' ? '<unknown>' : fallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const extractFeature = (mapping: string): Feature => {
|
|
42
|
+
if (mapping.startsWith('runtime') || mapping === 'root') {
|
|
43
|
+
return {name: 'runtime', type: FEATURE_TYPES.Runtime};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (mapping != null && mapping !== '') {
|
|
47
|
+
return {name: mapping, type: FEATURE_TYPES.Binary};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {name: EVERYTHING_ELSE, type: FEATURE_TYPES.Misc};
|
|
51
|
+
};
|
|
@@ -13,13 +13,16 @@
|
|
|
13
13
|
|
|
14
14
|
import {useEffect, useMemo} from 'react';
|
|
15
15
|
|
|
16
|
+
import {Table} from 'apache-arrow';
|
|
17
|
+
|
|
16
18
|
import {Flamegraph} from '@parca/client';
|
|
17
19
|
import {Button} from '@parca/components';
|
|
18
20
|
import {useContainerDimensions} from '@parca/hooks';
|
|
19
21
|
import {divide, selectQueryParam, type NavigateFunction} from '@parca/utilities';
|
|
20
22
|
|
|
21
23
|
import DiffLegend from '../components/DiffLegend';
|
|
22
|
-
import
|
|
24
|
+
import IcicleGraph from './IcicleGraph';
|
|
25
|
+
import IcicleGraphArrow from './IcicleGraphArrow';
|
|
23
26
|
|
|
24
27
|
const numberFormatter = new Intl.NumberFormat('en-US');
|
|
25
28
|
|
|
@@ -27,7 +30,8 @@ export type ResizeHandler = (width: number, height: number) => void;
|
|
|
27
30
|
|
|
28
31
|
interface ProfileIcicleGraphProps {
|
|
29
32
|
width?: number;
|
|
30
|
-
graph
|
|
33
|
+
graph?: Flamegraph;
|
|
34
|
+
table?: Table<any>;
|
|
31
35
|
total: bigint;
|
|
32
36
|
filtered: bigint;
|
|
33
37
|
sampleUnit: string;
|
|
@@ -40,6 +44,7 @@ interface ProfileIcicleGraphProps {
|
|
|
40
44
|
|
|
41
45
|
const ProfileIcicleGraph = ({
|
|
42
46
|
graph,
|
|
47
|
+
table,
|
|
43
48
|
total,
|
|
44
49
|
filtered,
|
|
45
50
|
curPath,
|
|
@@ -66,7 +71,8 @@ const ProfileIcicleGraph = ({
|
|
|
66
71
|
return ['0', '0', false, '0', '0', false, '0', '0'];
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
const trimmed = graph.trimmed;
|
|
74
|
+
// const trimmed = graph.trimmed;
|
|
75
|
+
const trimmed = 0n;
|
|
70
76
|
|
|
71
77
|
const totalUnfiltered = total + filtered;
|
|
72
78
|
// safeguard against division by zero
|
|
@@ -101,7 +107,7 @@ const ProfileIcicleGraph = ({
|
|
|
101
107
|
);
|
|
102
108
|
}, [setNewCurPath, curPath, setActionButtons]);
|
|
103
109
|
|
|
104
|
-
if (graph === undefined) return <div>no data...</div>;
|
|
110
|
+
if (graph === undefined && table === undefined) return <div>no data...</div>;
|
|
105
111
|
|
|
106
112
|
if (total === 0n && !loading) return <>Profile has no samples</>;
|
|
107
113
|
|
|
@@ -113,16 +119,30 @@ const ProfileIcicleGraph = ({
|
|
|
113
119
|
<div className="relative">
|
|
114
120
|
{compareMode && <DiffLegend />}
|
|
115
121
|
<div ref={ref}>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
{graph !== undefined && (
|
|
123
|
+
<IcicleGraph
|
|
124
|
+
width={dimensions?.width}
|
|
125
|
+
graph={graph}
|
|
126
|
+
total={total}
|
|
127
|
+
filtered={filtered}
|
|
128
|
+
curPath={curPath}
|
|
129
|
+
setCurPath={setNewCurPath}
|
|
130
|
+
sampleUnit={sampleUnit}
|
|
131
|
+
navigateTo={navigateTo}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
{table !== undefined && (
|
|
135
|
+
<IcicleGraphArrow
|
|
136
|
+
width={dimensions?.width}
|
|
137
|
+
table={table}
|
|
138
|
+
total={total}
|
|
139
|
+
filtered={filtered}
|
|
140
|
+
curPath={curPath}
|
|
141
|
+
setCurPath={setNewCurPath}
|
|
142
|
+
sampleUnit={sampleUnit}
|
|
143
|
+
navigateTo={navigateTo}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
126
146
|
</div>
|
|
127
147
|
<p className="my-2 text-xs">
|
|
128
148
|
Showing {totalFormatted}{' '}
|
|
@@ -22,8 +22,21 @@ import {getStepDuration} from '@parca/utilities';
|
|
|
22
22
|
|
|
23
23
|
import {MergedProfileSelection, ProfileSelection} from '..';
|
|
24
24
|
import MetricsGraph from '../MetricsGraph';
|
|
25
|
+
import {useMetricsGraphDimensions} from '../MetricsGraph/useMetricsGraphDimensions';
|
|
25
26
|
import useDelayedLoader from '../useDelayedLoader';
|
|
26
27
|
|
|
28
|
+
interface ProfileMetricsEmptyStateProps {
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const ProfileMetricsEmptyState = ({message}: ProfileMetricsEmptyStateProps): JSX.Element => {
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex h-full w-full flex-col items-center justify-center">
|
|
35
|
+
<p>{message}</p>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
27
40
|
interface ProfileMetricsGraphProps {
|
|
28
41
|
queryClient: QueryServiceClient;
|
|
29
42
|
queryExpression: string;
|
|
@@ -52,15 +65,12 @@ export const useQueryRange = (
|
|
|
52
65
|
isLoading: true,
|
|
53
66
|
error: null,
|
|
54
67
|
});
|
|
68
|
+
const [isLoading, setLoading] = useState<boolean>(true);
|
|
55
69
|
const metadata = useGrpcMetadata();
|
|
56
70
|
|
|
57
71
|
useEffect(() => {
|
|
58
72
|
void (async () => {
|
|
59
|
-
|
|
60
|
-
response: null,
|
|
61
|
-
isLoading: true,
|
|
62
|
-
error: null,
|
|
63
|
-
});
|
|
73
|
+
setLoading(true);
|
|
64
74
|
|
|
65
75
|
const stepDuration = getStepDuration(start, end);
|
|
66
76
|
const call = client.queryRange(
|
|
@@ -75,12 +85,19 @@ export const useQueryRange = (
|
|
|
75
85
|
);
|
|
76
86
|
|
|
77
87
|
call.response
|
|
78
|
-
.then(response =>
|
|
79
|
-
|
|
88
|
+
.then(response => {
|
|
89
|
+
setState({response, isLoading: false, error: null});
|
|
90
|
+
setLoading(false);
|
|
91
|
+
return null;
|
|
92
|
+
})
|
|
93
|
+
.catch(error => {
|
|
94
|
+
setState({response: null, isLoading: false, error});
|
|
95
|
+
setLoading(false);
|
|
96
|
+
});
|
|
80
97
|
})();
|
|
81
98
|
}, [client, queryExpression, start, end, metadata]);
|
|
82
99
|
|
|
83
|
-
return state;
|
|
100
|
+
return {...state, isLoading};
|
|
84
101
|
};
|
|
85
102
|
|
|
86
103
|
const ProfileMetricsGraph = ({
|
|
@@ -96,6 +113,7 @@ const ProfileMetricsGraph = ({
|
|
|
96
113
|
const {isLoading, response, error} = useQueryRange(queryClient, queryExpression, from, to);
|
|
97
114
|
const isLoaderVisible = useDelayedLoader(isLoading);
|
|
98
115
|
const {loader, onError, perf} = useParcaContext();
|
|
116
|
+
const {width, height, margin, marginRight} = useMetricsGraphDimensions();
|
|
99
117
|
|
|
100
118
|
useEffect(() => {
|
|
101
119
|
if (error !== null) {
|
|
@@ -111,7 +129,10 @@ const ProfileMetricsGraph = ({
|
|
|
111
129
|
perf?.markInteraction('Metrics graph render', response.series[0].samples.length);
|
|
112
130
|
}, [perf, response]);
|
|
113
131
|
|
|
114
|
-
|
|
132
|
+
const series = response?.series;
|
|
133
|
+
const dataAvailable = series !== null && series !== undefined && series?.length > 0;
|
|
134
|
+
|
|
135
|
+
if (isLoaderVisible || (isLoading && !dataAvailable)) {
|
|
115
136
|
return <>{loader}</>;
|
|
116
137
|
}
|
|
117
138
|
|
|
@@ -127,15 +148,14 @@ const ProfileMetricsGraph = ({
|
|
|
127
148
|
);
|
|
128
149
|
}
|
|
129
150
|
|
|
130
|
-
|
|
131
|
-
if (series !== null && series !== undefined && series?.length > 0) {
|
|
151
|
+
if (dataAvailable) {
|
|
132
152
|
const handleSampleClick = (timestamp: number, _value: number, labels: Label[]): void => {
|
|
133
153
|
onPointClick(timestamp, labels, queryExpression);
|
|
134
154
|
};
|
|
135
155
|
|
|
136
156
|
return (
|
|
137
157
|
<div
|
|
138
|
-
className="rounded border-gray-300 dark:border-gray-500 dark:bg-gray-700"
|
|
158
|
+
className="h-full w-full rounded border-gray-300 dark:border-gray-500 dark:bg-gray-700"
|
|
139
159
|
style={{borderWidth: 1}}
|
|
140
160
|
>
|
|
141
161
|
<MetricsGraph
|
|
@@ -146,19 +166,17 @@ const ProfileMetricsGraph = ({
|
|
|
146
166
|
setTimeRange={setTimeRange}
|
|
147
167
|
onSampleClick={handleSampleClick}
|
|
148
168
|
onLabelClick={addLabelMatcher}
|
|
149
|
-
width={0}
|
|
150
169
|
sampleUnit={Query.parse(queryExpression).profileType().sampleUnit}
|
|
170
|
+
height={height}
|
|
171
|
+
width={width}
|
|
172
|
+
margin={margin}
|
|
173
|
+
marginRight={marginRight}
|
|
151
174
|
/>
|
|
152
175
|
</div>
|
|
153
176
|
);
|
|
154
177
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
<div className="flex justify-center py-20">
|
|
158
|
-
<p className="m-0">No data found. Try a different query.</p>
|
|
159
|
-
</div>
|
|
160
|
-
</div>
|
|
161
|
-
);
|
|
178
|
+
|
|
179
|
+
return <ProfileMetricsEmptyState message="No data found. Try a different query." />;
|
|
162
180
|
};
|
|
163
181
|
|
|
164
182
|
export default ProfileMetricsGraph;
|
|
@@ -31,9 +31,11 @@ import {getStepDuration, getStepDurationInMilliseconds} from '@parca/utilities';
|
|
|
31
31
|
|
|
32
32
|
import {MergedProfileSelection, ProfileSelection} from '..';
|
|
33
33
|
import MatchersInput from '../MatchersInput/index';
|
|
34
|
-
import
|
|
34
|
+
import {useMetricsGraphDimensions} from '../MetricsGraph/useMetricsGraphDimensions';
|
|
35
|
+
import ProfileMetricsGraph, {ProfileMetricsEmptyState} from '../ProfileMetricsGraph';
|
|
35
36
|
import ProfileTypeSelector from '../ProfileTypeSelector/index';
|
|
36
37
|
import CompareButton from './CompareButton';
|
|
38
|
+
import {useAutoQuerySelector} from './useAutoQuerySelector';
|
|
37
39
|
|
|
38
40
|
export interface QuerySelection {
|
|
39
41
|
expression: string;
|
|
@@ -98,6 +100,7 @@ const ProfileSelector = ({
|
|
|
98
100
|
data: profileTypesData,
|
|
99
101
|
error,
|
|
100
102
|
} = useProfileTypes(queryClient);
|
|
103
|
+
const {heightStyle} = useMetricsGraphDimensions();
|
|
101
104
|
|
|
102
105
|
const [timeRangeSelection, setTimeRangeSelection] = useState(
|
|
103
106
|
DateTimeRange.fromRangeKey(querySelection.timeSelection)
|
|
@@ -179,6 +182,13 @@ const ProfileSelector = ({
|
|
|
179
182
|
}
|
|
180
183
|
};
|
|
181
184
|
|
|
185
|
+
useAutoQuerySelector({
|
|
186
|
+
selectedProfileName,
|
|
187
|
+
profileTypesData,
|
|
188
|
+
setProfileName,
|
|
189
|
+
setQueryExpression,
|
|
190
|
+
});
|
|
191
|
+
|
|
182
192
|
const handleCompareClick = (): void => onCompareProfile();
|
|
183
193
|
|
|
184
194
|
const searchDisabled =
|
|
@@ -236,61 +246,63 @@ const ProfileSelector = ({
|
|
|
236
246
|
</Card.Header>
|
|
237
247
|
{
|
|
238
248
|
<Card.Body>
|
|
239
|
-
{
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
setTimeRangeSelection(range);
|
|
257
|
-
selectQuery({
|
|
258
|
-
expression: queryExpressionString,
|
|
259
|
-
from,
|
|
260
|
-
to,
|
|
261
|
-
timeSelection: range.getRangeKey(),
|
|
262
|
-
...mergedProfileParams,
|
|
263
|
-
});
|
|
264
|
-
}}
|
|
265
|
-
addLabelMatcher={addLabelMatcher}
|
|
266
|
-
onPointClick={(timestamp, labels, queryExpression) => {
|
|
267
|
-
// TODO: Pass the query object via click rather than queryExpression
|
|
268
|
-
let query = Query.parse(queryExpression);
|
|
269
|
-
labels.forEach(l => {
|
|
270
|
-
const [newQuery, updated] = query.setMatcher(l.name, l.value);
|
|
271
|
-
if (updated) {
|
|
272
|
-
query = newQuery;
|
|
249
|
+
<div style={{height: heightStyle}}>
|
|
250
|
+
{querySelection.expression !== undefined &&
|
|
251
|
+
querySelection.expression.length > 0 &&
|
|
252
|
+
querySelection.from !== undefined &&
|
|
253
|
+
querySelection.to !== undefined ? (
|
|
254
|
+
<ProfileMetricsGraph
|
|
255
|
+
queryClient={queryClient}
|
|
256
|
+
queryExpression={querySelection.expression}
|
|
257
|
+
from={querySelection.from}
|
|
258
|
+
to={querySelection.to}
|
|
259
|
+
profile={profileSelection}
|
|
260
|
+
setTimeRange={(range: DateTimeRange) => {
|
|
261
|
+
const from = range.getFromMs();
|
|
262
|
+
const to = range.getToMs();
|
|
263
|
+
let mergedProfileParams = {};
|
|
264
|
+
if (query.profileType().delta) {
|
|
265
|
+
mergedProfileParams = {mergeFrom: from, mergeTo: to};
|
|
273
266
|
}
|
|
274
|
-
|
|
267
|
+
setTimeRangeSelection(range);
|
|
268
|
+
selectQuery({
|
|
269
|
+
expression: queryExpressionString,
|
|
270
|
+
from,
|
|
271
|
+
to,
|
|
272
|
+
timeSelection: range.getRangeKey(),
|
|
273
|
+
...mergedProfileParams,
|
|
274
|
+
});
|
|
275
|
+
}}
|
|
276
|
+
addLabelMatcher={addLabelMatcher}
|
|
277
|
+
onPointClick={(timestamp, labels, queryExpression) => {
|
|
278
|
+
// TODO: Pass the query object via click rather than queryExpression
|
|
279
|
+
let query = Query.parse(queryExpression);
|
|
280
|
+
labels.forEach(l => {
|
|
281
|
+
const [newQuery, updated] = query.setMatcher(l.name, l.value);
|
|
282
|
+
if (updated) {
|
|
283
|
+
query = newQuery;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
275
286
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
287
|
+
const stepDuration = getStepDuration(querySelection.from, querySelection.to);
|
|
288
|
+
const stepDurationInMilliseconds = getStepDurationInMilliseconds(stepDuration);
|
|
289
|
+
const mergeFrom = timestamp;
|
|
290
|
+
const mergeTo = query.profileType().delta
|
|
291
|
+
? mergeFrom + stepDurationInMilliseconds
|
|
292
|
+
: mergeFrom;
|
|
293
|
+
selectProfile(new MergedProfileSelection(mergeFrom, mergeTo, query));
|
|
294
|
+
}}
|
|
295
|
+
/>
|
|
296
|
+
) : (
|
|
297
|
+
<>
|
|
298
|
+
{profileSelection == null ? (
|
|
299
|
+
<ProfileMetricsEmptyState
|
|
300
|
+
message={`Please select a profile type and click "Search" to begin.`}
|
|
301
|
+
/>
|
|
302
|
+
) : null}
|
|
303
|
+
</>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
294
306
|
</Card.Body>
|
|
295
307
|
}
|
|
296
308
|
</Card>
|
|
@@ -0,0 +1,87 @@
|
|
|
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 {useEffect} from 'react';
|
|
15
|
+
|
|
16
|
+
import {ProfileTypesResponse} from '@parca/client';
|
|
17
|
+
import {selectAutoQuery, setAutoQuery, useAppDispatch, useAppSelector} from '@parca/store';
|
|
18
|
+
|
|
19
|
+
import {constructProfileName} from '../ProfileTypeSelector';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
selectedProfileName: string;
|
|
23
|
+
profileTypesData: ProfileTypesResponse | undefined;
|
|
24
|
+
setProfileName: (name: string) => void;
|
|
25
|
+
setQueryExpression: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const useAutoQuerySelector = ({
|
|
29
|
+
selectedProfileName,
|
|
30
|
+
profileTypesData,
|
|
31
|
+
setProfileName,
|
|
32
|
+
setQueryExpression,
|
|
33
|
+
}: Props): void => {
|
|
34
|
+
const autoQuery = useAppSelector(selectAutoQuery);
|
|
35
|
+
const dispatch = useAppDispatch();
|
|
36
|
+
|
|
37
|
+
// Effect to load some initial data on load when is no selection
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
void (async () => {
|
|
40
|
+
if (selectedProfileName.length > 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (profileTypesData?.types == null || profileTypesData.types.length < 1) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (autoQuery === 'true') {
|
|
47
|
+
// Autoquery already enabled.
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
dispatch(setAutoQuery('true'));
|
|
51
|
+
let profileType = profileTypesData.types.find(type => type.name === 'parca_agent_cpu');
|
|
52
|
+
if (profileType == null) {
|
|
53
|
+
profileType = profileTypesData.types[0];
|
|
54
|
+
}
|
|
55
|
+
setProfileName(constructProfileName(profileType));
|
|
56
|
+
})();
|
|
57
|
+
}, [
|
|
58
|
+
profileTypesData,
|
|
59
|
+
selectedProfileName,
|
|
60
|
+
autoQuery,
|
|
61
|
+
dispatch,
|
|
62
|
+
setQueryExpression,
|
|
63
|
+
setProfileName,
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
void (async () => {
|
|
68
|
+
if (
|
|
69
|
+
autoQuery !== 'true' ||
|
|
70
|
+
profileTypesData?.types == null ||
|
|
71
|
+
profileTypesData.types.length < 1 ||
|
|
72
|
+
selectedProfileName.length === 0
|
|
73
|
+
) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
setQueryExpression();
|
|
77
|
+
dispatch(setAutoQuery('false'));
|
|
78
|
+
})();
|
|
79
|
+
}, [
|
|
80
|
+
profileTypesData,
|
|
81
|
+
setQueryExpression,
|
|
82
|
+
autoQuery,
|
|
83
|
+
setProfileName,
|
|
84
|
+
dispatch,
|
|
85
|
+
selectedProfileName,
|
|
86
|
+
]);
|
|
87
|
+
};
|
|
@@ -128,17 +128,16 @@ export function profileSelectElement(
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
export const constructProfileName = (type: ProfileType): string => {
|
|
132
|
+
return `${type.name}:${type.sampleType}:${type.sampleUnit}:${type.periodType}:${type.periodUnit}${
|
|
133
|
+
type.delta ? ':delta' : ''
|
|
134
|
+
}`;
|
|
135
|
+
};
|
|
136
|
+
|
|
131
137
|
export const normalizeProfileTypesData = (types: ProfileType[]): string[] => {
|
|
132
|
-
return types
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
`${type.name}:${type.sampleType}:${type.sampleUnit}:${type.periodType}:${type.periodUnit}${
|
|
136
|
-
type.delta ? ':delta' : ''
|
|
137
|
-
}`
|
|
138
|
-
)
|
|
139
|
-
.sort((a: string, b: string): number => {
|
|
140
|
-
return a.localeCompare(b);
|
|
141
|
-
});
|
|
138
|
+
return types.map(constructProfileName).sort((a: string, b: string): number => {
|
|
139
|
+
return a.localeCompare(b);
|
|
140
|
+
});
|
|
142
141
|
};
|
|
143
142
|
|
|
144
143
|
interface Props {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import {Profiler, ProfilerProps, useEffect, useMemo, useState} from 'react';
|
|
15
15
|
|
|
16
|
+
import {Table} from 'apache-arrow';
|
|
16
17
|
import cx from 'classnames';
|
|
17
18
|
import {scaleLinear} from 'd3';
|
|
18
19
|
import graphviz from 'graphviz-wasm';
|
|
@@ -53,6 +54,7 @@ type NavigateFunction = (path: string, queryParams: any, options?: {replace?: bo
|
|
|
53
54
|
export interface FlamegraphData {
|
|
54
55
|
loading: boolean;
|
|
55
56
|
data?: Flamegraph;
|
|
57
|
+
table?: Table<any>;
|
|
56
58
|
total?: bigint;
|
|
57
59
|
filtered?: bigint;
|
|
58
60
|
error?: any;
|
|
@@ -113,7 +115,7 @@ export const ProfileView = ({
|
|
|
113
115
|
}: ProfileViewProps): JSX.Element => {
|
|
114
116
|
const {ref, dimensions} = useContainerDimensions();
|
|
115
117
|
const [curPath, setCurPath] = useState<string[]>([]);
|
|
116
|
-
const [rawDashboardItems, setDashboardItems] = useURLState({
|
|
118
|
+
const [rawDashboardItems = ['icicle'], setDashboardItems] = useURLState({
|
|
117
119
|
param: 'dashboard_items',
|
|
118
120
|
navigateTo,
|
|
119
121
|
});
|
|
@@ -230,7 +232,7 @@ export const ProfileView = ({
|
|
|
230
232
|
}): JSX.Element => {
|
|
231
233
|
switch (type) {
|
|
232
234
|
case 'icicle': {
|
|
233
|
-
return flamegraphData?.data
|
|
235
|
+
return flamegraphData?.table !== undefined || flamegraphData.data !== undefined ? (
|
|
234
236
|
<ConditionalWrapper<ProfilerProps>
|
|
235
237
|
condition={perf?.onRender != null}
|
|
236
238
|
WrapperComponent={Profiler}
|
|
@@ -242,6 +244,7 @@ export const ProfileView = ({
|
|
|
242
244
|
<ProfileIcicleGraph
|
|
243
245
|
curPath={curPath}
|
|
244
246
|
setNewCurPath={setNewCurPath}
|
|
247
|
+
table={flamegraphData.table}
|
|
245
248
|
graph={flamegraphData.data}
|
|
246
249
|
total={total}
|
|
247
250
|
filtered={filtered}
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import {useEffect, useMemo, useState} from 'react';
|
|
15
15
|
|
|
16
|
+
import {tableFromIPC} from 'apache-arrow';
|
|
17
|
+
|
|
16
18
|
import {QueryRequest_ReportType, QueryServiceClient} from '@parca/client';
|
|
17
19
|
import {useGrpcMetadata, useParcaContext, useURLState} from '@parca/components';
|
|
18
|
-
import {USER_PREFERENCES, useUserPreference} from '@parca/hooks';
|
|
20
|
+
import {USER_PREFERENCES, useUIFeatureFlag, useUserPreference} from '@parca/hooks';
|
|
19
21
|
import {saveAsBlob, type NavigateFunction} from '@parca/utilities';
|
|
20
22
|
|
|
21
23
|
import {ProfileSource} from './ProfileSource';
|
|
@@ -36,8 +38,10 @@ export const ProfileViewWithData = ({
|
|
|
36
38
|
navigateTo,
|
|
37
39
|
}: ProfileViewWithDataProps): JSX.Element => {
|
|
38
40
|
const metadata = useGrpcMetadata();
|
|
39
|
-
const [dashboardItems] = useURLState({param: 'dashboard_items', navigateTo});
|
|
41
|
+
const [dashboardItems = ['icicle']] = useURLState({param: 'dashboard_items', navigateTo});
|
|
42
|
+
|
|
40
43
|
const [enableTrimming] = useUserPreference<boolean>(USER_PREFERENCES.ENABLE_GRAPH_TRIMMING.key);
|
|
44
|
+
const [arrowFlamegraphEnabled] = useUIFeatureFlag('flamegraph-arrow');
|
|
41
45
|
const [pprofDownloading, setPprofDownloading] = useState<boolean>(false);
|
|
42
46
|
|
|
43
47
|
const nodeTrimThreshold = useMemo(() => {
|
|
@@ -53,11 +57,15 @@ export const ProfileViewWithData = ({
|
|
|
53
57
|
return (1 / width) * 100;
|
|
54
58
|
}, [enableTrimming]);
|
|
55
59
|
|
|
60
|
+
const reportType = arrowFlamegraphEnabled
|
|
61
|
+
? QueryRequest_ReportType.FLAMEGRAPH_ARROW
|
|
62
|
+
: QueryRequest_ReportType.FLAMEGRAPH_TABLE;
|
|
63
|
+
|
|
56
64
|
const {
|
|
57
65
|
isLoading: flamegraphLoading,
|
|
58
66
|
response: flamegraphResponse,
|
|
59
67
|
error: flamegraphError,
|
|
60
|
-
} = useQuery(queryClient, profileSource,
|
|
68
|
+
} = useQuery(queryClient, profileSource, reportType, {
|
|
61
69
|
skip: !dashboardItems.includes('icicle'),
|
|
62
70
|
nodeTrimThreshold,
|
|
63
71
|
});
|
|
@@ -80,16 +88,19 @@ export const ProfileViewWithData = ({
|
|
|
80
88
|
});
|
|
81
89
|
|
|
82
90
|
useEffect(() => {
|
|
83
|
-
if (
|
|
84
|
-
|
|
91
|
+
if (
|
|
92
|
+
(!flamegraphLoading && flamegraphResponse?.report.oneofKind === 'flamegraph') ||
|
|
93
|
+
flamegraphResponse?.report.oneofKind === 'flamegraphArrow'
|
|
94
|
+
) {
|
|
95
|
+
perf?.markInteraction('Flamegraph render', flamegraphResponse.total);
|
|
85
96
|
}
|
|
86
97
|
|
|
87
98
|
if (!topTableLoading && topTableResponse?.report.oneofKind === 'top') {
|
|
88
|
-
perf?.markInteraction('Top table render', topTableResponse
|
|
99
|
+
perf?.markInteraction('Top table render', topTableResponse.total);
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
if (!callgraphLoading && callgraphResponse?.report.oneofKind === 'callgraph') {
|
|
92
|
-
perf?.markInteraction('Callgraph render', callgraphResponse
|
|
103
|
+
perf?.markInteraction('Callgraph render', callgraphResponse.total);
|
|
93
104
|
}
|
|
94
105
|
}, [
|
|
95
106
|
flamegraphLoading,
|
|
@@ -144,6 +155,10 @@ export const ProfileViewWithData = ({
|
|
|
144
155
|
flamegraphResponse?.report.oneofKind === 'flamegraph'
|
|
145
156
|
? flamegraphResponse?.report?.flamegraph
|
|
146
157
|
: undefined,
|
|
158
|
+
table:
|
|
159
|
+
flamegraphResponse?.report.oneofKind === 'flamegraphArrow'
|
|
160
|
+
? tableFromIPC(flamegraphResponse?.report?.flamegraphArrow.record)
|
|
161
|
+
: undefined,
|
|
147
162
|
total: BigInt(flamegraphResponse?.total ?? '0'),
|
|
148
163
|
filtered: BigInt(flamegraphResponse?.filtered ?? '0'),
|
|
149
164
|
error: flamegraphError,
|