@parca/profile 0.19.113 → 0.19.115
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/ProfileExplorer/ProfileExplorerSingle.d.ts.map +1 -1
- package/dist/ProfileExplorer/ProfileExplorerSingle.js +9 -3
- package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.d.ts +31 -0
- package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.d.ts.map +1 -0
- package/dist/{MetricsGraphStrips/AreaGraph → ProfileFlameChart/SamplesStrips/SamplesGraph}/index.js +32 -60
- package/dist/{MetricsGraphStrips/MetricsGraphStrips.stories.d.ts → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.d.ts} +4 -3
- package/dist/ProfileFlameChart/SamplesStrips/SamplesStrips.stories.d.ts.map +1 -0
- package/dist/{MetricsGraphStrips/MetricsGraphStrips.stories.js → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.js} +5 -4
- package/dist/{MetricsGraphStrips → ProfileFlameChart/SamplesStrips}/index.d.ts +5 -4
- package/dist/ProfileFlameChart/SamplesStrips/index.d.ts.map +1 -0
- package/dist/ProfileFlameChart/SamplesStrips/index.js +145 -0
- package/dist/ProfileFlameChart/index.d.ts +20 -0
- package/dist/ProfileFlameChart/index.d.ts.map +1 -0
- package/dist/ProfileFlameChart/index.js +155 -0
- package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/index.js +0 -1
- package/dist/ProfileMetricsGraph/hooks/useQueryRange.d.ts +2 -1
- package/dist/ProfileMetricsGraph/hooks/useQueryRange.d.ts.map +1 -1
- package/dist/ProfileMetricsGraph/hooks/useQueryRange.js +11 -21
- package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
- package/dist/ProfileMetricsGraph/index.js +13 -3
- package/dist/ProfileSelector/index.d.ts.map +1 -1
- package/dist/ProfileSelector/index.js +4 -0
- package/dist/ProfileView/components/ActionButtons/GroupByDropdown.d.ts +1 -0
- package/dist/ProfileView/components/ActionButtons/GroupByDropdown.d.ts.map +1 -1
- package/dist/ProfileView/components/ActionButtons/GroupByDropdown.js +2 -2
- package/dist/ProfileView/components/DashboardItems/index.d.ts +5 -4
- package/dist/ProfileView/components/DashboardItems/index.d.ts.map +1 -1
- package/dist/ProfileView/components/DashboardItems/index.js +4 -3
- package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts +2 -1
- package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts.map +1 -1
- package/dist/ProfileView/components/GroupByLabelsDropdown/index.js +2 -2
- package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +1 -1
- package/dist/ProfileView/components/Toolbars/index.d.ts +2 -0
- package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
- package/dist/ProfileView/components/Toolbars/index.js +4 -2
- package/dist/ProfileView/hooks/useAutoSelectDimension.d.ts +16 -0
- package/dist/ProfileView/hooks/useAutoSelectDimension.d.ts.map +1 -0
- package/dist/ProfileView/hooks/useAutoSelectDimension.js +75 -0
- package/dist/ProfileView/hooks/useVisualizationState.d.ts +2 -0
- package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
- package/dist/ProfileView/hooks/useVisualizationState.js +8 -0
- package/dist/ProfileView/index.d.ts +1 -1
- package/dist/ProfileView/index.d.ts.map +1 -1
- package/dist/ProfileView/index.js +7 -4
- package/dist/ProfileView/types/visualization.d.ts +15 -3
- package/dist/ProfileView/types/visualization.d.ts.map +1 -1
- package/dist/ProfileViewWithData.d.ts +2 -1
- package/dist/ProfileViewWithData.d.ts.map +1 -1
- package/dist/ProfileViewWithData.js +41 -29
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/styles.css +1 -1
- package/package.json +8 -7
- package/src/ProfileExplorer/ProfileExplorerSingle.tsx +14 -3
- package/src/{MetricsGraphStrips/AreaGraph → ProfileFlameChart/SamplesStrips/SamplesGraph}/index.tsx +77 -81
- package/src/{MetricsGraphStrips/MetricsGraphStrips.stories.tsx → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.tsx} +7 -6
- package/src/ProfileFlameChart/SamplesStrips/index.tsx +317 -0
- package/src/ProfileFlameChart/index.tsx +305 -0
- package/src/ProfileFlameGraph/index.tsx +0 -1
- package/src/ProfileMetricsGraph/hooks/useQueryRange.ts +18 -26
- package/src/ProfileMetricsGraph/index.tsx +24 -2
- package/src/ProfileSelector/index.tsx +11 -0
- package/src/ProfileView/components/ActionButtons/GroupByDropdown.tsx +3 -0
- package/src/ProfileView/components/DashboardItems/index.tsx +19 -17
- package/src/ProfileView/components/GroupByLabelsDropdown/index.tsx +4 -2
- package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +1 -1
- package/src/ProfileView/components/Toolbars/index.tsx +18 -1
- package/src/ProfileView/hooks/useAutoSelectDimension.ts +90 -0
- package/src/ProfileView/hooks/useVisualizationState.ts +17 -0
- package/src/ProfileView/index.tsx +16 -2
- package/src/ProfileView/types/visualization.ts +17 -3
- package/src/ProfileViewWithData.tsx +80 -37
- package/src/index.tsx +4 -0
- package/dist/MetricsGraphStrips/AreaGraph/Tooltip.d.ts +0 -10
- package/dist/MetricsGraphStrips/AreaGraph/Tooltip.d.ts.map +0 -1
- package/dist/MetricsGraphStrips/AreaGraph/Tooltip.js +0 -44
- package/dist/MetricsGraphStrips/AreaGraph/index.d.ts +0 -21
- package/dist/MetricsGraphStrips/AreaGraph/index.d.ts.map +0 -1
- package/dist/MetricsGraphStrips/MetricsGraphStrips.stories.d.ts.map +0 -1
- package/dist/MetricsGraphStrips/index.d.ts.map +0 -1
- package/dist/MetricsGraphStrips/index.js +0 -70
- package/src/MetricsGraphStrips/AreaGraph/Tooltip.tsx +0 -83
- package/src/MetricsGraphStrips/index.tsx +0 -142
|
@@ -0,0 +1,305 @@
|
|
|
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, useMemo, useRef} from 'react';
|
|
15
|
+
|
|
16
|
+
import {LabelSet, QueryRequest_ReportType, QueryServiceClient} from '@parca/client';
|
|
17
|
+
import {
|
|
18
|
+
Button,
|
|
19
|
+
useParcaContext,
|
|
20
|
+
useURLState,
|
|
21
|
+
useURLStateCustom,
|
|
22
|
+
type OptionsCustom,
|
|
23
|
+
} from '@parca/components';
|
|
24
|
+
import {Matcher, MatcherTypes, ProfileType, Query} from '@parca/parser';
|
|
25
|
+
|
|
26
|
+
import ProfileFlameGraph, {validateFlameChartQuery} from '../ProfileFlameGraph';
|
|
27
|
+
import {boundsFromProfileSource} from '../ProfileFlameGraph/FlameGraphArrow/utils';
|
|
28
|
+
import {MergedProfileSource, ProfileSource} from '../ProfileSource';
|
|
29
|
+
import type {SamplesData} from '../ProfileView/types/visualization';
|
|
30
|
+
import {useQuery} from '../useQuery';
|
|
31
|
+
import {NumberDuo} from '../utils';
|
|
32
|
+
import {SamplesStrip} from './SamplesStrips';
|
|
33
|
+
|
|
34
|
+
interface SelectedTimeframe {
|
|
35
|
+
labels: LabelSet;
|
|
36
|
+
bounds: NumberDuo;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const TimeframeStateSerializer: OptionsCustom<SelectedTimeframe | undefined> = {
|
|
40
|
+
parse: (value: string | string[] | undefined) => {
|
|
41
|
+
if (value == null || value === '' || value === 'undefined' || Array.isArray(value)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const [labelPart, boundsPart] = value.split('|');
|
|
46
|
+
if (labelPart != null && boundsPart != null) {
|
|
47
|
+
const labels = labelPart.split(',').map(labelStr => {
|
|
48
|
+
const [name, ...rest] = labelStr.split(':');
|
|
49
|
+
return {name, value: rest.join(':')};
|
|
50
|
+
});
|
|
51
|
+
const [startMs, endMs] = boundsPart.split(',').map(Number);
|
|
52
|
+
if (labels.length > 0 && !isNaN(startMs) && !isNaN(endMs)) {
|
|
53
|
+
return {
|
|
54
|
+
labels: {labels},
|
|
55
|
+
bounds: [startMs, endMs] as NumberDuo,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Ignore parsing errors
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
},
|
|
64
|
+
stringify: (value: SelectedTimeframe | undefined) => {
|
|
65
|
+
if (value == null) {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
const labelsStr = value.labels.labels.map(l => `${l.name}:${l.value}`).join(',');
|
|
69
|
+
return `${labelsStr}|${value.bounds[0]},${value.bounds[1]}`;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
interface ProfileFlameChartProps {
|
|
74
|
+
samplesData?: SamplesData;
|
|
75
|
+
queryClient: QueryServiceClient;
|
|
76
|
+
profileSource: ProfileSource;
|
|
77
|
+
width: number;
|
|
78
|
+
total: bigint;
|
|
79
|
+
filtered: bigint;
|
|
80
|
+
profileType?: ProfileType;
|
|
81
|
+
isHalfScreen: boolean;
|
|
82
|
+
metadataMappingFiles?: string[];
|
|
83
|
+
metadataLoading?: boolean;
|
|
84
|
+
onSwitchToOneMinute?: () => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Helper to create a filtered profile source with narrowed time bounds
|
|
88
|
+
// and dimension label matchers from the selected strip.
|
|
89
|
+
const createFilteredProfileSource = (
|
|
90
|
+
profileSource: ProfileSource,
|
|
91
|
+
selectedTimeframe: {labels: LabelSet; bounds: NumberDuo}
|
|
92
|
+
): ProfileSource | null => {
|
|
93
|
+
if (!(profileSource instanceof MergedProfileSource)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The bounds are in milliseconds, convert to nanoseconds for the profile source
|
|
98
|
+
// Round to integers since BigInt requires integer values
|
|
99
|
+
const mergeFrom = BigInt(Math.round(selectedTimeframe.bounds[0])) * 1_000_000n;
|
|
100
|
+
const mergeTo = BigInt(Math.round(selectedTimeframe.bounds[1])) * 1_000_000n;
|
|
101
|
+
|
|
102
|
+
// Add dimension labels as additional matchers to the query
|
|
103
|
+
const dimensionMatchers = selectedTimeframe.labels.labels.map(
|
|
104
|
+
l => new Matcher(l.name, MatcherTypes.MatchEqual, l.value)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const query = new Query(
|
|
108
|
+
profileSource.query.profType,
|
|
109
|
+
[...profileSource.query.matchers, ...dimensionMatchers],
|
|
110
|
+
''
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return new MergedProfileSource(mergeFrom, mergeTo, query);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const ProfileFlameChart = ({
|
|
117
|
+
samplesData,
|
|
118
|
+
queryClient,
|
|
119
|
+
profileSource,
|
|
120
|
+
width,
|
|
121
|
+
total,
|
|
122
|
+
filtered,
|
|
123
|
+
profileType,
|
|
124
|
+
isHalfScreen,
|
|
125
|
+
metadataMappingFiles,
|
|
126
|
+
metadataLoading,
|
|
127
|
+
onSwitchToOneMinute,
|
|
128
|
+
}: ProfileFlameChartProps): JSX.Element => {
|
|
129
|
+
const {loader} = useParcaContext();
|
|
130
|
+
|
|
131
|
+
const [selectedTimeframe, setSelectedTimeframe] = useURLStateCustom<
|
|
132
|
+
SelectedTimeframe | undefined
|
|
133
|
+
>('flamechart_timeframe', TimeframeStateSerializer);
|
|
134
|
+
|
|
135
|
+
// Read flamechart dimension from URL state to detect changes
|
|
136
|
+
const [flamechartDimension] = useURLState<string[]>('flamechart_dimension', {
|
|
137
|
+
alwaysReturnArray: true,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Reset selection when the parent time range (profileSource) changes
|
|
141
|
+
const timeBoundsKey = boundsFromProfileSource(profileSource).join(',');
|
|
142
|
+
const prevTimeBoundsKey = useRef(timeBoundsKey);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (prevTimeBoundsKey.current !== timeBoundsKey) {
|
|
145
|
+
prevTimeBoundsKey.current = timeBoundsKey;
|
|
146
|
+
setSelectedTimeframe(undefined);
|
|
147
|
+
}
|
|
148
|
+
}, [timeBoundsKey, setSelectedTimeframe]);
|
|
149
|
+
|
|
150
|
+
// Reset selection when the dimension changes
|
|
151
|
+
const dimensionKey = (flamechartDimension ?? []).join(',');
|
|
152
|
+
const prevDimensionKey = useRef(dimensionKey);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (prevDimensionKey.current !== dimensionKey) {
|
|
155
|
+
prevDimensionKey.current = dimensionKey;
|
|
156
|
+
setSelectedTimeframe(undefined);
|
|
157
|
+
}
|
|
158
|
+
}, [dimensionKey, setSelectedTimeframe]);
|
|
159
|
+
|
|
160
|
+
// Handle timeframe selection from strips
|
|
161
|
+
const handleSelectedTimeframe = (labels: LabelSet, bounds: NumberDuo | undefined): void => {
|
|
162
|
+
if (bounds === undefined) {
|
|
163
|
+
setSelectedTimeframe(undefined);
|
|
164
|
+
} else {
|
|
165
|
+
setSelectedTimeframe({labels, bounds});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Create filtered profile source when selection exists
|
|
170
|
+
const filteredProfileSource = useMemo(() => {
|
|
171
|
+
if (selectedTimeframe == null) return null;
|
|
172
|
+
return createFilteredProfileSource(profileSource, selectedTimeframe);
|
|
173
|
+
}, [profileSource, selectedTimeframe]);
|
|
174
|
+
|
|
175
|
+
// Query flamechart data only when a strip selection exists
|
|
176
|
+
const {
|
|
177
|
+
isLoading: flamechartLoading,
|
|
178
|
+
response: flamechartResponse,
|
|
179
|
+
error: flamechartError,
|
|
180
|
+
} = useQuery(
|
|
181
|
+
queryClient,
|
|
182
|
+
filteredProfileSource ?? profileSource,
|
|
183
|
+
QueryRequest_ReportType.FLAMECHART,
|
|
184
|
+
{
|
|
185
|
+
skip: selectedTimeframe == null || filteredProfileSource == null,
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const flamechartArrow =
|
|
190
|
+
flamechartResponse?.report.oneofKind === 'flamegraphArrow'
|
|
191
|
+
? flamechartResponse.report.flamegraphArrow
|
|
192
|
+
: undefined;
|
|
193
|
+
const flamechartTotal = flamechartResponse != null ? BigInt(flamechartResponse.total) : total;
|
|
194
|
+
const flamechartFiltered =
|
|
195
|
+
flamechartResponse != null ? BigInt(flamechartResponse.filtered) : filtered;
|
|
196
|
+
|
|
197
|
+
// Get time bounds from profile source for the strips
|
|
198
|
+
const timeBounds = boundsFromProfileSource(profileSource);
|
|
199
|
+
|
|
200
|
+
// Transform samples data for SamplesStrip component
|
|
201
|
+
const stripsData = useMemo(() => {
|
|
202
|
+
if (samplesData?.series == null) return {cpus: [], data: [], stepMs: 0};
|
|
203
|
+
|
|
204
|
+
const cpus = samplesData.series.map(s => s.labelset);
|
|
205
|
+
const data = samplesData.series.map(s => s.data);
|
|
206
|
+
|
|
207
|
+
const stepMs = samplesData.stepMs ?? 0;
|
|
208
|
+
|
|
209
|
+
return {cpus, data, stepMs};
|
|
210
|
+
}, [samplesData?.series, samplesData?.stepMs]);
|
|
211
|
+
|
|
212
|
+
const {isValid, isNonDelta, isDurationTooLong} = validateFlameChartQuery(
|
|
213
|
+
profileSource as MergedProfileSource
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (!isValid) {
|
|
217
|
+
if (isDurationTooLong) {
|
|
218
|
+
return (
|
|
219
|
+
<div className="flex flex-col justify-center items-center p-10 text-center gap-4 text-sm">
|
|
220
|
+
<span>
|
|
221
|
+
Flame chart is unavailable for queries longer than one minute. Try reducing the time
|
|
222
|
+
range to one minute or selecting a point in the metrics graph.
|
|
223
|
+
</span>
|
|
224
|
+
{onSwitchToOneMinute != null && (
|
|
225
|
+
<Button variant="primary" onClick={onSwitchToOneMinute}>
|
|
226
|
+
Switch to last 1 minute
|
|
227
|
+
</Button>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const message = isNonDelta
|
|
234
|
+
? 'To use the Flame chart, please switch to a Delta profile.'
|
|
235
|
+
: 'Flame chart is unavailable for this query.';
|
|
236
|
+
return (
|
|
237
|
+
<div className="flex flex-col justify-center p-10 text-center gap-6 text-sm">{message}</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const hasDimension = (flamechartDimension ?? []).length > 0;
|
|
242
|
+
|
|
243
|
+
// Show loader while metadata labels are loading (needed for dimension auto-selection)
|
|
244
|
+
if (metadataLoading === true) {
|
|
245
|
+
return <>{loader}</>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!hasDimension) {
|
|
249
|
+
return (
|
|
250
|
+
<div className="flex justify-center items-center py-10 text-gray-500 dark:text-gray-400 text-sm">
|
|
251
|
+
Select a label in the "Samples group by" dropdown above to view the samples
|
|
252
|
+
strips.
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (samplesData?.loading === true) {
|
|
258
|
+
return <>{loader}</>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<div>
|
|
263
|
+
{/* Samples Strips - rendered above flamechart */}
|
|
264
|
+
{stripsData.cpus.length > 0 && stripsData.data.length > 0 && (
|
|
265
|
+
<div className="mb-2">
|
|
266
|
+
<SamplesStrip
|
|
267
|
+
cpus={stripsData.cpus}
|
|
268
|
+
data={stripsData.data}
|
|
269
|
+
selectedTimeframe={selectedTimeframe}
|
|
270
|
+
onSelectedTimeframe={handleSelectedTimeframe}
|
|
271
|
+
width={width}
|
|
272
|
+
bounds={[Number(timeBounds[0] / 1_000_000n), Number(timeBounds[1] / 1_000_000n)]}
|
|
273
|
+
stepMs={stripsData.stepMs}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{/* Flamegraph visualization - only shown when a time range is selected in the strips */}
|
|
279
|
+
{selectedTimeframe != null && filteredProfileSource != null ? (
|
|
280
|
+
<ProfileFlameGraph
|
|
281
|
+
arrow={flamechartArrow}
|
|
282
|
+
loading={flamechartLoading}
|
|
283
|
+
error={flamechartError}
|
|
284
|
+
profileSource={filteredProfileSource}
|
|
285
|
+
width={width}
|
|
286
|
+
total={flamechartTotal}
|
|
287
|
+
filtered={flamechartFiltered}
|
|
288
|
+
profileType={profileType}
|
|
289
|
+
isHalfScreen={isHalfScreen}
|
|
290
|
+
metadataMappingFiles={metadataMappingFiles}
|
|
291
|
+
metadataLoading={metadataLoading}
|
|
292
|
+
isFlameChart={true}
|
|
293
|
+
curPathArrow={[]}
|
|
294
|
+
setNewCurPathArrow={() => {}}
|
|
295
|
+
/>
|
|
296
|
+
) : (
|
|
297
|
+
<div className="flex justify-center items-center py-10 text-gray-500 dark:text-gray-400 text-sm">
|
|
298
|
+
Select a time range in the samples strips above to view the flamechart.
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export default ProfileFlameChart;
|
|
@@ -77,7 +77,6 @@ export const validateFlameChartQuery = (
|
|
|
77
77
|
): {isValid: boolean; isNonDelta: boolean; isDurationTooLong: boolean} => {
|
|
78
78
|
const isNonDelta = !profileSource.ProfileType().delta;
|
|
79
79
|
const duration = profileSource.mergeTo - profileSource.mergeFrom;
|
|
80
|
-
console.log('duration of flame chart query: ', duration, 'ns');
|
|
81
80
|
const isDurationTooLong = duration > 60_000_000_000n; // 60 seconds in nanoseconds
|
|
82
81
|
return {isValid: !isNonDelta && !isDurationTooLong, isNonDelta, isDurationTooLong};
|
|
83
82
|
};
|
|
@@ -11,20 +11,24 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {useEffect, useMemo} from 'react';
|
|
15
|
-
|
|
16
14
|
import {RpcError} from '@protobuf-ts/runtime-rpc';
|
|
17
15
|
|
|
18
16
|
import {Duration, QueryRangeResponse, QueryServiceClient, Timestamp} from '@parca/client';
|
|
19
|
-
import {useGrpcMetadata
|
|
20
|
-
import {getStepDuration} from '@parca/utilities';
|
|
17
|
+
import {useGrpcMetadata} from '@parca/components';
|
|
18
|
+
import {getStepDuration, getStepDurationInMilliseconds} from '@parca/utilities';
|
|
21
19
|
|
|
22
20
|
import useGrpcQuery from '../../useGrpcQuery';
|
|
23
21
|
|
|
22
|
+
interface QueryRangeResult {
|
|
23
|
+
response: QueryRangeResponse;
|
|
24
|
+
stepDurationMs: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
24
27
|
interface IQueryRangeState {
|
|
25
28
|
response: QueryRangeResponse | null;
|
|
26
29
|
isLoading: boolean;
|
|
27
30
|
error: RpcError | null;
|
|
31
|
+
stepDurationMs: number;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export const getStepCountFromScreenWidth = (pixelsPerPoint: number): number => {
|
|
@@ -43,33 +47,16 @@ export const useQueryRange = (
|
|
|
43
47
|
start: number,
|
|
44
48
|
end: number,
|
|
45
49
|
sumBy: string[],
|
|
50
|
+
stepCount: number,
|
|
46
51
|
skip = false
|
|
47
52
|
): IQueryRangeState => {
|
|
48
53
|
const metadata = useGrpcMetadata();
|
|
49
|
-
const [stepCountStr, setStepCount] = useURLState('step_count');
|
|
50
|
-
|
|
51
|
-
const defaultStepCount = useMemo(() => {
|
|
52
|
-
return getStepCountFromScreenWidth(10);
|
|
53
|
-
}, []);
|
|
54
|
-
|
|
55
|
-
const stepCount = useMemo(() => {
|
|
56
|
-
if (stepCountStr != null) {
|
|
57
|
-
return parseInt(stepCountStr as string, 10);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return defaultStepCount;
|
|
61
|
-
}, [stepCountStr, defaultStepCount]);
|
|
62
|
-
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
if (stepCountStr == null) {
|
|
65
|
-
setStepCount(defaultStepCount.toString());
|
|
66
|
-
}
|
|
67
|
-
}, [stepCountStr, defaultStepCount, setStepCount]);
|
|
68
54
|
|
|
69
|
-
const {data, isLoading, error} = useGrpcQuery<
|
|
55
|
+
const {data, isLoading, error} = useGrpcQuery<QueryRangeResult | undefined>({
|
|
70
56
|
key: ['query-range', queryExpression, start, end, (sumBy ?? []).join(','), stepCount, metadata],
|
|
71
57
|
queryFn: async signal => {
|
|
72
58
|
const stepDuration = getStepDuration(start, end, stepCount);
|
|
59
|
+
const stepDurationMs = getStepDurationInMilliseconds(stepDuration);
|
|
73
60
|
const {response} = await client.queryRange(
|
|
74
61
|
{
|
|
75
62
|
query: queryExpression,
|
|
@@ -81,7 +68,7 @@ export const useQueryRange = (
|
|
|
81
68
|
},
|
|
82
69
|
{meta: metadata, abort: signal}
|
|
83
70
|
);
|
|
84
|
-
return response;
|
|
71
|
+
return {response, stepDurationMs};
|
|
85
72
|
},
|
|
86
73
|
options: {
|
|
87
74
|
retry: false,
|
|
@@ -90,5 +77,10 @@ export const useQueryRange = (
|
|
|
90
77
|
},
|
|
91
78
|
});
|
|
92
79
|
|
|
93
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
isLoading,
|
|
82
|
+
error: error as RpcError | null,
|
|
83
|
+
response: data?.response ?? null,
|
|
84
|
+
stepDurationMs: data?.stepDurationMs ?? 0,
|
|
85
|
+
};
|
|
94
86
|
};
|
|
@@ -25,8 +25,11 @@ import {
|
|
|
25
25
|
import {
|
|
26
26
|
DateTimeRange,
|
|
27
27
|
MetricsGraphSkeleton,
|
|
28
|
+
NumberParser,
|
|
29
|
+
NumberSerializer,
|
|
28
30
|
TextWithTooltip,
|
|
29
31
|
useParcaContext,
|
|
32
|
+
useURLStateCustom,
|
|
30
33
|
} from '@parca/components';
|
|
31
34
|
import {Query} from '@parca/parser';
|
|
32
35
|
import {TEST_IDS, testId} from '@parca/test-utils';
|
|
@@ -35,7 +38,7 @@ import {capitalizeOnlyFirstLetter, formatDate, timePattern, valueFormatter} from
|
|
|
35
38
|
import {MergedProfileSelection, ProfileSelection} from '..';
|
|
36
39
|
import MetricsGraph, {ContextMenuItemOrSubmenu, Series, SeriesPoint} from '../MetricsGraph';
|
|
37
40
|
import {useMetricsGraphDimensions} from '../MetricsGraph/useMetricsGraphDimensions';
|
|
38
|
-
import {useQueryRange} from './hooks/useQueryRange';
|
|
41
|
+
import {getStepCountFromScreenWidth, useQueryRange} from './hooks/useQueryRange';
|
|
39
42
|
|
|
40
43
|
const createProfileContextMenuItems = (
|
|
41
44
|
addLabelMatcher: (
|
|
@@ -197,11 +200,30 @@ const ProfileMetricsGraph = ({
|
|
|
197
200
|
comparing = false,
|
|
198
201
|
sumBy,
|
|
199
202
|
}: ProfileMetricsGraphProps): JSX.Element => {
|
|
203
|
+
const [rawStepCount] = useURLStateCustom<number>('step_count', {
|
|
204
|
+
defaultValue: String(getStepCountFromScreenWidth(10)),
|
|
205
|
+
parse: NumberParser,
|
|
206
|
+
stringify: NumberSerializer,
|
|
207
|
+
});
|
|
208
|
+
// Clamp step count so the step duration is at least 1 second as we don't have this enforced server-side anymore.
|
|
209
|
+
const stepCount = useMemo(() => {
|
|
210
|
+
const maxForOneSecond = Math.floor((to - from) / 1000);
|
|
211
|
+
return Math.min(rawStepCount, maxForOneSecond);
|
|
212
|
+
}, [rawStepCount, from, to]);
|
|
213
|
+
|
|
200
214
|
const {
|
|
201
215
|
isLoading: metricsGraphLoading,
|
|
202
216
|
response,
|
|
203
217
|
error,
|
|
204
|
-
} = useQueryRange(
|
|
218
|
+
} = useQueryRange(
|
|
219
|
+
queryClient,
|
|
220
|
+
queryExpression,
|
|
221
|
+
from,
|
|
222
|
+
to,
|
|
223
|
+
sumBy,
|
|
224
|
+
stepCount,
|
|
225
|
+
queryExpression === ''
|
|
226
|
+
);
|
|
205
227
|
const {onError, perf, authenticationErrorMessage, isDarkMode, timezone} = useParcaContext();
|
|
206
228
|
const {width, height, margin, heightStyle} = useMetricsGraphDimensions(comparing);
|
|
207
229
|
const [showAllSeriesForResponse, setShowAllSeriesForResponse] = useState<typeof response | null>(
|
|
@@ -153,6 +153,17 @@ const ProfileSelector = ({
|
|
|
153
153
|
DateTimeRange.fromRangeKey(draftSelection.timeSelection, draftSelection.from, draftSelection.to)
|
|
154
154
|
);
|
|
155
155
|
|
|
156
|
+
// Sync local timeRangeSelection when URL state changes externally (e.g., "Switch to 1 minute" button)
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
setTimeRangeSelection(
|
|
159
|
+
DateTimeRange.fromRangeKey(
|
|
160
|
+
querySelection.timeSelection,
|
|
161
|
+
querySelection.from,
|
|
162
|
+
querySelection.to
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
}, [querySelection.timeSelection, querySelection.from, querySelection.to]);
|
|
166
|
+
|
|
156
167
|
const [queryExpressionString, setQueryExpressionString] = useState(draftSelection.expression);
|
|
157
168
|
|
|
158
169
|
const [advancedModeForQueryBrowser, setAdvancedModeForQueryBrowser] = useState(
|
|
@@ -21,6 +21,7 @@ interface GroupByControlsProps {
|
|
|
21
21
|
setGroupByLabels: (labels: string[]) => void;
|
|
22
22
|
metadataRefetch?: () => Promise<void>;
|
|
23
23
|
metadataLoading: boolean;
|
|
24
|
+
label?: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const GroupByControls: React.FC<GroupByControlsProps> = ({
|
|
@@ -29,6 +30,7 @@ const GroupByControls: React.FC<GroupByControlsProps> = ({
|
|
|
29
30
|
setGroupByLabels,
|
|
30
31
|
metadataRefetch,
|
|
31
32
|
metadataLoading,
|
|
33
|
+
label,
|
|
32
34
|
}) => {
|
|
33
35
|
return (
|
|
34
36
|
<div className="relative flex" id="h-group-by-controls">
|
|
@@ -38,6 +40,7 @@ const GroupByControls: React.FC<GroupByControlsProps> = ({
|
|
|
38
40
|
setGroupByLabels={setGroupByLabels}
|
|
39
41
|
metadataRefetch={metadataRefetch}
|
|
40
42
|
metadataLoading={metadataLoading}
|
|
43
|
+
label={label}
|
|
41
44
|
/>
|
|
42
45
|
</div>
|
|
43
46
|
);
|
|
@@ -16,6 +16,7 @@ import {Profiler, ProfilerOnRenderCallback} from 'react';
|
|
|
16
16
|
import {QueryServiceClient} from '@parca/client';
|
|
17
17
|
import {ConditionalWrapper} from '@parca/components';
|
|
18
18
|
|
|
19
|
+
import ProfileFlameChart from '../../../ProfileFlameChart';
|
|
19
20
|
import ProfileFlameGraph from '../../../ProfileFlameGraph';
|
|
20
21
|
import {CurrentPathFrame} from '../../../ProfileFlameGraph/FlameGraphArrow/utils';
|
|
21
22
|
import {ProfileSource} from '../../../ProfileSource';
|
|
@@ -24,6 +25,7 @@ import {SourceView} from '../../../SourceView';
|
|
|
24
25
|
import {Table} from '../../../Table';
|
|
25
26
|
import type {
|
|
26
27
|
FlamegraphData,
|
|
28
|
+
SamplesData,
|
|
27
29
|
SandwichData,
|
|
28
30
|
SourceData,
|
|
29
31
|
TopTableData,
|
|
@@ -35,7 +37,7 @@ interface GetDashboardItemProps {
|
|
|
35
37
|
isHalfScreen: boolean;
|
|
36
38
|
dimensions: DOMRect | undefined;
|
|
37
39
|
flamegraphData: FlamegraphData;
|
|
38
|
-
|
|
40
|
+
samplesData?: SamplesData;
|
|
39
41
|
topTableData?: TopTableData;
|
|
40
42
|
sandwichData: SandwichData;
|
|
41
43
|
sourceData?: SourceData;
|
|
@@ -47,7 +49,8 @@ interface GetDashboardItemProps {
|
|
|
47
49
|
perf?: {
|
|
48
50
|
onRender?: ProfilerOnRenderCallback;
|
|
49
51
|
};
|
|
50
|
-
queryClient
|
|
52
|
+
queryClient: QueryServiceClient;
|
|
53
|
+
onSwitchToOneMinute?: () => void;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
export const getDashboardItem = ({
|
|
@@ -55,7 +58,7 @@ export const getDashboardItem = ({
|
|
|
55
58
|
isHalfScreen,
|
|
56
59
|
dimensions,
|
|
57
60
|
flamegraphData,
|
|
58
|
-
|
|
61
|
+
samplesData,
|
|
59
62
|
topTableData,
|
|
60
63
|
sourceData,
|
|
61
64
|
sandwichData,
|
|
@@ -65,6 +68,8 @@ export const getDashboardItem = ({
|
|
|
65
68
|
curPathArrow,
|
|
66
69
|
setNewCurPathArrow,
|
|
67
70
|
perf,
|
|
71
|
+
queryClient,
|
|
72
|
+
onSwitchToOneMinute,
|
|
68
73
|
}: GetDashboardItemProps): JSX.Element => {
|
|
69
74
|
switch (type) {
|
|
70
75
|
case 'flamegraph':
|
|
@@ -102,16 +107,10 @@ export const getDashboardItem = ({
|
|
|
102
107
|
);
|
|
103
108
|
case 'flamechart':
|
|
104
109
|
return (
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
total={total}
|
|
110
|
-
filtered={filtered}
|
|
111
|
-
profileType={profileSource?.ProfileType()}
|
|
112
|
-
loading={flamechartData.loading}
|
|
113
|
-
error={flamechartData.error}
|
|
114
|
-
isHalfScreen={isHalfScreen}
|
|
110
|
+
<ProfileFlameChart
|
|
111
|
+
samplesData={samplesData}
|
|
112
|
+
queryClient={queryClient}
|
|
113
|
+
profileSource={profileSource}
|
|
115
114
|
width={
|
|
116
115
|
dimensions?.width !== undefined
|
|
117
116
|
? isHalfScreen
|
|
@@ -119,10 +118,13 @@ export const getDashboardItem = ({
|
|
|
119
118
|
: dimensions.width - 16
|
|
120
119
|
: 0
|
|
121
120
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
total={total}
|
|
122
|
+
filtered={filtered}
|
|
123
|
+
profileType={profileSource?.ProfileType()}
|
|
124
|
+
isHalfScreen={isHalfScreen}
|
|
125
|
+
metadataMappingFiles={flamegraphData.metadataMappingFiles}
|
|
126
|
+
metadataLoading={flamegraphData.metadataLoading}
|
|
127
|
+
onSwitchToOneMinute={onSwitchToOneMinute}
|
|
126
128
|
/>
|
|
127
129
|
);
|
|
128
130
|
case 'table':
|
|
@@ -27,6 +27,7 @@ interface Props {
|
|
|
27
27
|
setGroupByLabels: (labels: string[]) => void;
|
|
28
28
|
metadataRefetch?: () => Promise<void>;
|
|
29
29
|
metadataLoading: boolean;
|
|
30
|
+
label?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const GroupByLabelsDropdown = ({
|
|
@@ -35,12 +36,13 @@ const GroupByLabelsDropdown = ({
|
|
|
35
36
|
setGroupByLabels,
|
|
36
37
|
metadataRefetch,
|
|
37
38
|
metadataLoading,
|
|
39
|
+
label = 'Group by',
|
|
38
40
|
}: Props): JSX.Element => {
|
|
39
41
|
return (
|
|
40
42
|
<div className="flex flex-col relative" {...testId(TEST_IDS.GROUP_BY_CONTAINER)}>
|
|
41
43
|
<div className="flex items-center justify-between">
|
|
42
44
|
<label className="text-sm" {...testId(TEST_IDS.GROUP_BY_LABEL)}>
|
|
43
|
-
|
|
45
|
+
{label}
|
|
44
46
|
</label>
|
|
45
47
|
</div>
|
|
46
48
|
|
|
@@ -56,7 +58,7 @@ const GroupByLabelsDropdown = ({
|
|
|
56
58
|
refreshTitle="Refresh label names"
|
|
57
59
|
refreshTestId="group-by-refresh-button"
|
|
58
60
|
menuTestId={TEST_IDS.GROUP_BY_SELECT_FLYOUT}
|
|
59
|
-
value={groupBy
|
|
61
|
+
value={(groupBy ?? [])
|
|
60
62
|
.filter(l => l.startsWith(FIELD_LABELS))
|
|
61
63
|
.map(l => ({value: l, label: l.slice(FIELD_LABELS.length + 1)}))}
|
|
62
64
|
onChange={newValue => {
|
|
@@ -412,7 +412,7 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
|
|
412
412
|
<Menu.Items
|
|
413
413
|
className={cx(
|
|
414
414
|
isTableVizOnly ? 'w-64' : 'w-80',
|
|
415
|
-
'absolute z-
|
|
415
|
+
'absolute z-50 mt-2 py-2 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none border dark:bg-gray-900 dark:border-gray-600',
|
|
416
416
|
shouldOpenLeft ? 'right-0 origin-top-right' : 'left-0 origin-top-left'
|
|
417
417
|
)}
|
|
418
418
|
>
|
|
@@ -47,6 +47,8 @@ export interface VisualisationToolbarProps {
|
|
|
47
47
|
preferencesModal?: boolean;
|
|
48
48
|
profileViewExternalSubActions?: React.ReactNode;
|
|
49
49
|
setGroupByLabels: (labels: string[]) => void;
|
|
50
|
+
flamechartDimension: string[];
|
|
51
|
+
setFlamechartDimension: (labels: string[]) => void;
|
|
50
52
|
showVisualizationSelector?: boolean;
|
|
51
53
|
sandwichFunctionName?: string;
|
|
52
54
|
alignFunctionName: string;
|
|
@@ -135,6 +137,8 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
|
|
|
135
137
|
groupBy,
|
|
136
138
|
toggleGroupBy,
|
|
137
139
|
setGroupByLabels,
|
|
140
|
+
flamechartDimension,
|
|
141
|
+
setFlamechartDimension,
|
|
138
142
|
profileType,
|
|
139
143
|
profileSource,
|
|
140
144
|
queryClient,
|
|
@@ -158,6 +162,8 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
|
|
|
158
162
|
const isTableVizOnly = dashboardItems?.length === 1 && isTableViz;
|
|
159
163
|
const isGraphViz = dashboardItems?.includes('flamegraph');
|
|
160
164
|
const isGraphVizOnly = dashboardItems?.length === 1 && isGraphViz;
|
|
165
|
+
const isFlamechartViz = dashboardItems?.includes('flamechart');
|
|
166
|
+
const isFlamechartVizOnly = dashboardItems?.length === 1 && isFlamechartViz;
|
|
161
167
|
|
|
162
168
|
const req = profileSource?.QueryRequest();
|
|
163
169
|
if (req !== null && req !== undefined) {
|
|
@@ -184,8 +190,19 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
|
|
|
184
190
|
</>
|
|
185
191
|
)}
|
|
186
192
|
|
|
193
|
+
{isFlamechartViz && (
|
|
194
|
+
<GroupByDropdown
|
|
195
|
+
groupBy={flamechartDimension}
|
|
196
|
+
labels={groupByLabels}
|
|
197
|
+
setGroupByLabels={setFlamechartDimension}
|
|
198
|
+
metadataRefetch={metadataRefetch}
|
|
199
|
+
metadataLoading={metadataLoading}
|
|
200
|
+
label="Samples group by"
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
203
|
+
|
|
187
204
|
<div className="flex mt-5">
|
|
188
|
-
<ProfileFilters />
|
|
205
|
+
{!isFlamechartVizOnly && <ProfileFilters />}
|
|
189
206
|
|
|
190
207
|
{profileViewExternalSubActions != null ? profileViewExternalSubActions : null}
|
|
191
208
|
</div>
|