@parca/profile 0.19.95 → 0.19.103
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 +32 -0
- package/dist/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.js +65 -45
- package/dist/ProfileFlameGraph/FlameGraphArrow/index.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/FlameGraphArrow/index.js +16 -4
- package/dist/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.d.ts +11 -0
- package/dist/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.d.ts.map +1 -0
- package/dist/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.js +65 -0
- package/dist/ProfileFlameGraph/FlameGraphArrow/useScrollViewport.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/FlameGraphArrow/useScrollViewport.js +35 -5
- package/dist/ProfileFlameGraph/FlameGraphArrow/useVisibleNodes.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/FlameGraphArrow/useVisibleNodes.js +29 -3
- package/dist/ProfileSelector/MetricsGraphSection.d.ts +1 -1
- package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
- package/dist/ProfileSelector/MetricsGraphSection.js +1 -1
- package/dist/ProfileSelector/index.d.ts.map +1 -1
- package/dist/ProfileSelector/index.js +1 -2
- package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.d.ts.map +1 -1
- package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.js +8 -0
- package/dist/SimpleMatchers/Select.d.ts.map +1 -1
- package/dist/SimpleMatchers/Select.js +3 -3
- package/dist/hooks/useLabels.d.ts.map +1 -1
- package/dist/hooks/useLabels.js +7 -2
- package/dist/hooks/useQueryState.d.ts.map +1 -1
- package/dist/hooks/useQueryState.js +53 -23
- package/dist/hooks/useQueryState.test.js +32 -22
- package/dist/styles.css +1 -1
- package/dist/useSumBy.d.ts +10 -2
- package/dist/useSumBy.d.ts.map +1 -1
- package/dist/useSumBy.js +30 -7
- package/package.json +15 -10
- package/src/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.tsx +89 -57
- package/src/ProfileFlameGraph/FlameGraphArrow/index.tsx +27 -2
- package/src/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.ts +84 -0
- package/src/ProfileFlameGraph/FlameGraphArrow/useScrollViewport.ts +40 -5
- package/src/ProfileFlameGraph/FlameGraphArrow/useVisibleNodes.ts +41 -5
- package/src/ProfileSelector/MetricsGraphSection.tsx +2 -2
- package/src/ProfileSelector/index.tsx +1 -5
- package/src/ProfileView/hooks/useResetStateOnProfileTypeChange.ts +8 -0
- package/src/SimpleMatchers/Select.tsx +3 -3
- package/src/hooks/useLabels.ts +8 -2
- package/src/hooks/useQueryState.test.tsx +41 -22
- package/src/hooks/useQueryState.ts +72 -31
- package/src/useSumBy.ts +58 -4
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parca/profile",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.103",
|
|
4
4
|
"description": "Profile viewing libraries",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@floating-ui/react": "^0.27.12",
|
|
7
7
|
"@headlessui/react": "^1.7.19",
|
|
8
8
|
"@iconify/react": "^4.0.0",
|
|
9
|
-
"@parca/client": "0.17.
|
|
10
|
-
"@parca/components": "0.16.
|
|
11
|
-
"@parca/dynamicsize": "0.16.
|
|
12
|
-
"@parca/hooks": "0.0.
|
|
13
|
-
"@parca/icons": "0.16.
|
|
14
|
-
"@parca/parser": "0.16.
|
|
15
|
-
"@parca/store": "0.16.
|
|
9
|
+
"@parca/client": "0.17.16",
|
|
10
|
+
"@parca/components": "0.16.392",
|
|
11
|
+
"@parca/dynamicsize": "0.16.72",
|
|
12
|
+
"@parca/hooks": "0.0.116",
|
|
13
|
+
"@parca/icons": "0.16.79",
|
|
14
|
+
"@parca/parser": "0.16.86",
|
|
15
|
+
"@parca/store": "0.16.199",
|
|
16
16
|
"@parca/test-utils": "0.0.17",
|
|
17
|
-
"@parca/utilities": "0.0.
|
|
17
|
+
"@parca/utilities": "0.0.122",
|
|
18
18
|
"@popperjs/core": "^2.11.8",
|
|
19
19
|
"@protobuf-ts/runtime-rpc": "^2.5.0",
|
|
20
20
|
"@storybook/preview-api": "^8.4.3",
|
|
@@ -75,9 +75,14 @@
|
|
|
75
75
|
"keywords": [],
|
|
76
76
|
"author": "",
|
|
77
77
|
"license": "ISC",
|
|
78
|
+
"repository": {
|
|
79
|
+
"type": "git",
|
|
80
|
+
"url": "https://github.com/parca-dev/parca",
|
|
81
|
+
"directory": "ui/packages/shared/profile"
|
|
82
|
+
},
|
|
78
83
|
"publishConfig": {
|
|
79
84
|
"access": "public",
|
|
80
85
|
"registry": "https://registry.npmjs.org/"
|
|
81
86
|
},
|
|
82
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "14996d0bb0fdf1193c1555e4d171aefd5138303b"
|
|
83
88
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import React, {useMemo} from 'react';
|
|
14
|
+
import React, {useCallback, useMemo} from 'react';
|
|
15
15
|
|
|
16
16
|
import {Table} from 'apache-arrow';
|
|
17
17
|
import cx from 'classnames';
|
|
@@ -101,36 +101,52 @@ export const FlameNode = React.memo(
|
|
|
101
101
|
effectiveDepth,
|
|
102
102
|
tooltipId = 'default',
|
|
103
103
|
}: FlameNodeProps): React.JSX.Element {
|
|
104
|
-
//
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
// Memoize column references - only changes when table changes
|
|
105
|
+
const columns = useMemo(
|
|
106
|
+
() => ({
|
|
107
|
+
mapping: table.getChild(FIELD_MAPPING_FILE),
|
|
108
|
+
functionName: table.getChild(FIELD_FUNCTION_NAME),
|
|
109
|
+
cumulative: table.getChild(FIELD_CUMULATIVE),
|
|
110
|
+
depth: table.getChild(FIELD_DEPTH),
|
|
111
|
+
diff: table.getChild(FIELD_DIFF),
|
|
112
|
+
filename: table.getChild(FIELD_FUNCTION_FILE_NAME),
|
|
113
|
+
valueOffset: table.getChild(FIELD_VALUE_OFFSET),
|
|
114
|
+
ts: table.getChild(FIELD_TIMESTAMP),
|
|
115
|
+
}),
|
|
116
|
+
[table]
|
|
117
|
+
);
|
|
113
118
|
|
|
114
119
|
// get the actual values from the columns
|
|
115
120
|
const binaries = useAppSelector(selectBinaries);
|
|
116
121
|
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
// Memoize row data extraction - only changes when table or row changes
|
|
123
|
+
const rowData = useMemo(() => {
|
|
124
|
+
const mappingFile: string | null = arrowToString(columns.mapping?.get(row));
|
|
125
|
+
const functionName: string | null = arrowToString(columns.functionName?.get(row));
|
|
126
|
+
const cumulative =
|
|
127
|
+
columns.cumulative?.get(row) != null ? BigInt(columns.cumulative?.get(row)) : 0n;
|
|
128
|
+
const diff: bigint | null =
|
|
129
|
+
columns.diff?.get(row) != null ? BigInt(columns.diff?.get(row)) : null;
|
|
130
|
+
const filename: string | null = arrowToString(columns.filename?.get(row));
|
|
131
|
+
const depth: number = columns.depth?.get(row) ?? 0;
|
|
132
|
+
const valueOffset: bigint =
|
|
133
|
+
columns.valueOffset?.get(row) !== null && columns.valueOffset?.get(row) !== undefined
|
|
134
|
+
? BigInt(columns.valueOffset?.get(row))
|
|
135
|
+
: 0n;
|
|
136
|
+
|
|
137
|
+
return {mappingFile, functionName, cumulative, diff, filename, depth, valueOffset};
|
|
138
|
+
}, [columns, row]);
|
|
139
|
+
|
|
140
|
+
const {mappingFile, functionName, cumulative, diff, filename, depth, valueOffset} = rowData;
|
|
128
141
|
|
|
129
142
|
const colorAttribute =
|
|
130
143
|
colorBy === 'filename' ? filename : colorBy === 'binary' ? mappingFile : null;
|
|
131
144
|
|
|
132
|
-
|
|
133
|
-
|
|
145
|
+
// Memoize hovering name lookup
|
|
146
|
+
const hoveringName = useMemo(() => {
|
|
147
|
+
return hoveringRow !== undefined ? arrowToString(columns.functionName?.get(hoveringRow)) : '';
|
|
148
|
+
}, [columns.functionName, hoveringRow]);
|
|
149
|
+
|
|
134
150
|
const shouldBeHighlighted =
|
|
135
151
|
functionName != null && hoveringName != null && functionName === hoveringName;
|
|
136
152
|
|
|
@@ -147,18 +163,59 @@ export const FlameNode = React.memo(
|
|
|
147
163
|
return row === 0 ? 'root' : nodeLabel(table, row, binaries.length > 1);
|
|
148
164
|
}, [table, row, binaries]);
|
|
149
165
|
|
|
166
|
+
// Memoize selection data - only changes when selectedRow changes
|
|
167
|
+
const selectionData = useMemo(() => {
|
|
168
|
+
const selectionOffset =
|
|
169
|
+
columns.valueOffset?.get(selectedRow) !== null &&
|
|
170
|
+
columns.valueOffset?.get(selectedRow) !== undefined
|
|
171
|
+
? BigInt(columns.valueOffset?.get(selectedRow))
|
|
172
|
+
: 0n;
|
|
173
|
+
const selectionCumulative =
|
|
174
|
+
columns.cumulative?.get(selectedRow) !== null
|
|
175
|
+
? BigInt(columns.cumulative?.get(selectedRow))
|
|
176
|
+
: 0n;
|
|
177
|
+
const selectedDepth = columns.depth?.get(selectedRow);
|
|
178
|
+
const total = columns.cumulative?.get(selectedRow);
|
|
179
|
+
return {selectionOffset, selectionCumulative, selectedDepth, total};
|
|
180
|
+
}, [columns, selectedRow]);
|
|
181
|
+
|
|
182
|
+
const {selectionOffset, selectionCumulative, selectedDepth, total} = selectionData;
|
|
183
|
+
|
|
184
|
+
// Memoize tsBounds - only changes when profileSource changes
|
|
185
|
+
const tsBounds = useMemo(() => boundsFromProfileSource(profileSource), [profileSource]);
|
|
186
|
+
|
|
187
|
+
// Memoize event handlers
|
|
188
|
+
const onMouseEnter = useCallback((): void => {
|
|
189
|
+
setHoveringRow(row);
|
|
190
|
+
window.dispatchEvent(
|
|
191
|
+
new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
|
|
192
|
+
detail: {row},
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
}, [setHoveringRow, row, tooltipId]);
|
|
196
|
+
|
|
197
|
+
const onMouseLeave = useCallback((): void => {
|
|
198
|
+
setHoveringRow(undefined);
|
|
199
|
+
window.dispatchEvent(
|
|
200
|
+
new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
|
|
201
|
+
detail: {row: null},
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
}, [setHoveringRow, tooltipId]);
|
|
205
|
+
|
|
206
|
+
const handleContextMenu = useCallback(
|
|
207
|
+
(e: React.MouseEvent): void => {
|
|
208
|
+
onContextMenu(e, row);
|
|
209
|
+
},
|
|
210
|
+
[onContextMenu, row]
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Early returns - all hooks must be called before this point
|
|
150
214
|
// Hide frames beyond effective depth limit
|
|
151
215
|
if (effectiveDepth !== undefined && depth > effectiveDepth) {
|
|
152
216
|
return <></>;
|
|
153
217
|
}
|
|
154
218
|
|
|
155
|
-
const selectionOffset =
|
|
156
|
-
valueOffsetColumn?.get(selectedRow) !== null &&
|
|
157
|
-
valueOffsetColumn?.get(selectedRow) !== undefined
|
|
158
|
-
? BigInt(valueOffsetColumn?.get(selectedRow))
|
|
159
|
-
: 0n;
|
|
160
|
-
const selectionCumulative =
|
|
161
|
-
cumulativeColumn?.get(selectedRow) !== null ? BigInt(cumulativeColumn?.get(selectedRow)) : 0n;
|
|
162
219
|
if (
|
|
163
220
|
valueOffset + cumulative <= selectionOffset ||
|
|
164
221
|
valueOffset >= selectionOffset + selectionCumulative
|
|
@@ -173,8 +230,6 @@ export const FlameNode = React.memo(
|
|
|
173
230
|
}
|
|
174
231
|
|
|
175
232
|
// Cumulative can be larger than total when a selection is made. All parents of the selection are likely larger, but we want to only show them as 100% in the graph.
|
|
176
|
-
const tsBounds = boundsFromProfileSource(profileSource);
|
|
177
|
-
const total = cumulativeColumn?.get(selectedRow);
|
|
178
233
|
const totalRatio = cumulative > total ? 1 : Number(cumulative) / Number(total);
|
|
179
234
|
const width: number = isFlameChart
|
|
180
235
|
? (Number(cumulative) / (Number(tsBounds[1]) - Number(tsBounds[0]))) * totalWidth
|
|
@@ -184,35 +239,12 @@ export const FlameNode = React.memo(
|
|
|
184
239
|
return <></>;
|
|
185
240
|
}
|
|
186
241
|
|
|
187
|
-
const selectedDepth = depthColumn?.get(selectedRow);
|
|
188
242
|
const styles =
|
|
189
243
|
selectedDepth !== undefined && selectedDepth > depth ? fadedFlameRectStyles : flameRectStyles;
|
|
190
244
|
|
|
191
|
-
const
|
|
192
|
-
setHoveringRow(row);
|
|
193
|
-
window.dispatchEvent(
|
|
194
|
-
new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
|
|
195
|
-
detail: {row},
|
|
196
|
-
})
|
|
197
|
-
);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const onMouseLeave = (): void => {
|
|
201
|
-
setHoveringRow(undefined);
|
|
202
|
-
window.dispatchEvent(
|
|
203
|
-
new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
|
|
204
|
-
detail: {row: null},
|
|
205
|
-
})
|
|
206
|
-
);
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
const handleContextMenu = (e: React.MouseEvent): void => {
|
|
210
|
-
onContextMenu(e, row);
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const ts = tsColumn !== null ? Number(tsColumn.get(row)) : 0;
|
|
245
|
+
const ts = columns.ts !== null ? Number(columns.ts.get(row)) : 0;
|
|
214
246
|
const x =
|
|
215
|
-
isFlameChart &&
|
|
247
|
+
isFlameChart && columns.ts !== null
|
|
216
248
|
? ((ts - Number(tsBounds[0])) / (Number(tsBounds[1]) - Number(tsBounds[0]))) * totalWidth
|
|
217
249
|
: selectedDepth > depth
|
|
218
250
|
? 0
|
|
@@ -25,7 +25,7 @@ import {Table, tableFromIPC} from 'apache-arrow';
|
|
|
25
25
|
import {useContextMenu} from 'react-contexify';
|
|
26
26
|
|
|
27
27
|
import {FlamegraphArrow} from '@parca/client';
|
|
28
|
-
import {useParcaContext} from '@parca/components';
|
|
28
|
+
import {FlameGraphSkeleton, SandwichFlameGraphSkeleton, useParcaContext} from '@parca/components';
|
|
29
29
|
import {USER_PREFERENCES, useCurrentColorProfile, useUserPreference} from '@parca/hooks';
|
|
30
30
|
import {ProfileType} from '@parca/parser';
|
|
31
31
|
import {getColorForFeature, selectDarkMode, useAppSelector} from '@parca/store';
|
|
@@ -38,6 +38,7 @@ import ContextMenuWrapper, {ContextMenuWrapperRef} from './ContextMenuWrapper';
|
|
|
38
38
|
import {FlameNode, RowHeight, colorByColors} from './FlameGraphNodes';
|
|
39
39
|
import {MemoizedTooltip} from './MemoizedTooltip';
|
|
40
40
|
import {TooltipProvider} from './TooltipContext';
|
|
41
|
+
import {useBatchedRendering} from './useBatchedRendering';
|
|
41
42
|
import {useScrollViewport} from './useScrollViewport';
|
|
42
43
|
import {useVisibleNodes} from './useVisibleNodes';
|
|
43
44
|
import {
|
|
@@ -136,6 +137,7 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
|
|
|
136
137
|
isFlameChart = false,
|
|
137
138
|
isRenderedAsFlamegraph = false,
|
|
138
139
|
isInSandwichView = false,
|
|
140
|
+
isHalfScreen,
|
|
139
141
|
tooltipId = 'default',
|
|
140
142
|
maxFrameCount,
|
|
141
143
|
isExpanded = false,
|
|
@@ -163,6 +165,7 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
|
|
|
163
165
|
const svg = useRef(null);
|
|
164
166
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
165
167
|
const renderStartTime = useRef<number>(0);
|
|
168
|
+
const hasInitialRenderCompleted = useRef(false);
|
|
166
169
|
|
|
167
170
|
const [svgElement, setSvgElement] = useState<SVGSVGElement | null>(null);
|
|
168
171
|
|
|
@@ -291,6 +294,18 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
|
|
|
291
294
|
effectiveDepth: deferredEffectiveDepth,
|
|
292
295
|
});
|
|
293
296
|
|
|
297
|
+
// Add nodes in incremental batches to avoid blocking the UI
|
|
298
|
+
const {items: batchedNodes, isComplete: isBatchingComplete} = useBatchedRendering(visibleNodes, {
|
|
299
|
+
batchSize: 500,
|
|
300
|
+
});
|
|
301
|
+
if (isBatchingComplete) {
|
|
302
|
+
hasInitialRenderCompleted.current = true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Show skeleton only during initial load, not during scroll updates
|
|
306
|
+
const showSkeleton =
|
|
307
|
+
!hasInitialRenderCompleted.current && batchedNodes.length !== visibleNodes.length;
|
|
308
|
+
|
|
294
309
|
useEffect(() => {
|
|
295
310
|
if (perf?.markInteraction != null) {
|
|
296
311
|
renderStartTime.current = performance.now();
|
|
@@ -327,12 +342,22 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
|
|
|
327
342
|
isInSandwichView={isInSandwichView}
|
|
328
343
|
/>
|
|
329
344
|
<MemoizedTooltip contextElement={svgElement} dockedMetainfo={dockedMetainfo} />
|
|
345
|
+
{showSkeleton && (
|
|
346
|
+
<div className="absolute inset-0 z-10">
|
|
347
|
+
{isRenderedAsFlamegraph ? (
|
|
348
|
+
<SandwichFlameGraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
|
|
349
|
+
) : (
|
|
350
|
+
<FlameGraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
330
354
|
<div
|
|
331
355
|
ref={containerRef}
|
|
332
356
|
className="overflow-auto scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-100 dark:scrollbar-thumb-gray-600 dark:scrollbar-track-gray-800 will-change-transform scroll-smooth webkit-overflow-scrolling-touch contain"
|
|
333
357
|
style={{
|
|
334
358
|
width: width ?? '100%',
|
|
335
359
|
contain: 'layout style paint',
|
|
360
|
+
visibility: !showSkeleton ? 'visible' : 'hidden',
|
|
336
361
|
}}
|
|
337
362
|
>
|
|
338
363
|
<svg
|
|
@@ -342,7 +367,7 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
|
|
|
342
367
|
preserveAspectRatio="xMinYMid"
|
|
343
368
|
ref={svg}
|
|
344
369
|
>
|
|
345
|
-
{
|
|
370
|
+
{batchedNodes.map(row => (
|
|
346
371
|
<FlameNode
|
|
347
372
|
key={row}
|
|
348
373
|
table={table}
|
|
@@ -0,0 +1,84 @@
|
|
|
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, useRef, useState} from 'react';
|
|
15
|
+
|
|
16
|
+
interface UseBatchedRenderingOptions {
|
|
17
|
+
batchSize?: number;
|
|
18
|
+
// Delay between batches in ms (0 = next animation frame)
|
|
19
|
+
batchDelay?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface UseBatchedRenderingResult<T> {
|
|
23
|
+
items: T[];
|
|
24
|
+
isComplete: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// useBatchedRendering - Helps in incrementally rendering items in batches to avoid UI blocking.
|
|
28
|
+
export const useBatchedRendering = <T>(
|
|
29
|
+
items: T[],
|
|
30
|
+
options: UseBatchedRenderingOptions = {}
|
|
31
|
+
): UseBatchedRenderingResult<T> => {
|
|
32
|
+
const {batchSize = 500, batchDelay = 0} = options;
|
|
33
|
+
|
|
34
|
+
const [renderedCount, setRenderedCount] = useState(0);
|
|
35
|
+
const itemsRef = useRef(items);
|
|
36
|
+
const rafRef = useRef<number | null>(null);
|
|
37
|
+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (itemsRef.current !== items) {
|
|
41
|
+
itemsRef.current = items;
|
|
42
|
+
setRenderedCount(prev => {
|
|
43
|
+
if (items.length === 0) return 0;
|
|
44
|
+
// If new items were added (scrolling down), keep current progress
|
|
45
|
+
if (items.length > prev) return prev;
|
|
46
|
+
// If items reduced, cap to new length
|
|
47
|
+
return Math.min(prev, items.length);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}, [items]);
|
|
51
|
+
|
|
52
|
+
// Progressively render more items
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (renderedCount === items.length) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const scheduleNextBatch = (): void => {
|
|
59
|
+
const incrementState = (): void => {
|
|
60
|
+
setRenderedCount(prev => Math.min(prev + batchSize, items.length));
|
|
61
|
+
};
|
|
62
|
+
if (batchDelay > 0) {
|
|
63
|
+
timeoutRef.current = setTimeout(incrementState, batchDelay);
|
|
64
|
+
} else {
|
|
65
|
+
rafRef.current = requestAnimationFrame(incrementState);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
scheduleNextBatch();
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
if (rafRef.current !== null) {
|
|
72
|
+
cancelAnimationFrame(rafRef.current);
|
|
73
|
+
}
|
|
74
|
+
if (timeoutRef.current !== null) {
|
|
75
|
+
clearTimeout(timeoutRef.current);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}, [renderedCount, items.length, batchSize, batchDelay]);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
items: items.slice(0, renderedCount),
|
|
82
|
+
isComplete: renderedCount === items.length,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
@@ -20,6 +20,21 @@ export interface ViewportState {
|
|
|
20
20
|
containerWidth: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// Find the scrollable ancestor (the element with overflow: auto/scroll)
|
|
24
|
+
const findScrollableParent = (element: HTMLElement | null): HTMLElement | undefined => {
|
|
25
|
+
if (element === null) return undefined;
|
|
26
|
+
let current: HTMLElement | null = element.parentElement;
|
|
27
|
+
while (current !== null) {
|
|
28
|
+
const style = window.getComputedStyle(current);
|
|
29
|
+
const overflowY = style.overflowY;
|
|
30
|
+
if (overflowY === 'auto' || overflowY === 'scroll') {
|
|
31
|
+
return current;
|
|
32
|
+
}
|
|
33
|
+
current = current.parentElement;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
23
38
|
export const useScrollViewport = (containerRef: React.RefObject<HTMLDivElement>): ViewportState => {
|
|
24
39
|
const [viewport, setViewport] = useState<ViewportState>({
|
|
25
40
|
scrollTop: 0,
|
|
@@ -33,11 +48,25 @@ export const useScrollViewport = (containerRef: React.RefObject<HTMLDivElement>)
|
|
|
33
48
|
const updateViewport = useCallback(() => {
|
|
34
49
|
if (containerRef.current !== null) {
|
|
35
50
|
const container = containerRef.current;
|
|
51
|
+
const rect = container.getBoundingClientRect();
|
|
52
|
+
|
|
53
|
+
// Restrict container height to the visible portion on screen
|
|
54
|
+
// This handles cases where the container is partially off-screen
|
|
55
|
+
// We only want to consider the visible part for culling calculations
|
|
56
|
+
|
|
57
|
+
const containerTop = rect.top;
|
|
58
|
+
const containerBottom = rect.bottom;
|
|
59
|
+
const viewportTop = 0;
|
|
60
|
+
const viewportBottom = window.innerHeight;
|
|
61
|
+
const visibleTop = Math.max(containerTop, viewportTop);
|
|
62
|
+
const visibleBottom = Math.min(containerBottom, viewportBottom);
|
|
63
|
+
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
|
64
|
+
const scrollOffset = Math.max(0, viewportTop - containerTop);
|
|
36
65
|
|
|
37
66
|
const newViewport = {
|
|
38
|
-
scrollTop:
|
|
67
|
+
scrollTop: scrollOffset,
|
|
39
68
|
scrollLeft: container.scrollLeft,
|
|
40
|
-
containerHeight:
|
|
69
|
+
containerHeight: visibleHeight, // Only the visible portion
|
|
41
70
|
containerWidth: container.clientWidth,
|
|
42
71
|
};
|
|
43
72
|
|
|
@@ -59,6 +88,8 @@ export const useScrollViewport = (containerRef: React.RefObject<HTMLDivElement>)
|
|
|
59
88
|
const container = containerRef.current;
|
|
60
89
|
if (container === null) return;
|
|
61
90
|
|
|
91
|
+
const scrollableParent = findScrollableParent(container);
|
|
92
|
+
|
|
62
93
|
// ResizeObserver Strategy:
|
|
63
94
|
// Monitor container size changes (window resize, layout shifts)
|
|
64
95
|
// to update viewport dimensions for accurate culling calculations
|
|
@@ -66,10 +97,12 @@ export const useScrollViewport = (containerRef: React.RefObject<HTMLDivElement>)
|
|
|
66
97
|
throttledUpdateViewport();
|
|
67
98
|
});
|
|
68
99
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
100
|
+
// Listen to scroll on the actual scrollable parent
|
|
101
|
+
|
|
102
|
+
scrollableParent?.addEventListener('scroll', throttledUpdateViewport, {passive: true});
|
|
72
103
|
container.addEventListener('scroll', throttledUpdateViewport, {passive: true});
|
|
104
|
+
window.addEventListener('scroll', throttledUpdateViewport, {passive: true});
|
|
105
|
+
|
|
73
106
|
resizeObserver.observe(container);
|
|
74
107
|
|
|
75
108
|
// Initialize viewport state on mount
|
|
@@ -77,7 +110,9 @@ export const useScrollViewport = (containerRef: React.RefObject<HTMLDivElement>)
|
|
|
77
110
|
|
|
78
111
|
return () => {
|
|
79
112
|
// Cleanup: Remove event listeners and cancel pending animations
|
|
113
|
+
scrollableParent?.removeEventListener('scroll', throttledUpdateViewport);
|
|
80
114
|
container.removeEventListener('scroll', throttledUpdateViewport);
|
|
115
|
+
window.removeEventListener('scroll', throttledUpdateViewport);
|
|
81
116
|
resizeObserver.disconnect();
|
|
82
117
|
if (throttleRef.current !== null) {
|
|
83
118
|
cancelAnimationFrame(throttleRef.current);
|
|
@@ -85,7 +85,19 @@ export const useVisibleNodes = ({
|
|
|
85
85
|
result: number[];
|
|
86
86
|
}>({key: '', result: []});
|
|
87
87
|
|
|
88
|
+
const renderedRangeRef = useRef<{minDepth: number; maxDepth: number; table: Table<any> | null}>({
|
|
89
|
+
minDepth: Infinity,
|
|
90
|
+
maxDepth: -Infinity,
|
|
91
|
+
table: null,
|
|
92
|
+
});
|
|
93
|
+
|
|
88
94
|
return useMemo(() => {
|
|
95
|
+
// This happens when the continer is scrolled off screen, in this case we return all previously rendered nodes
|
|
96
|
+
// to avoid trimming the rendered nodes to zero which would cause jank when scrolling back into view
|
|
97
|
+
if (viewport.containerHeight === 0 && lastResultRef.current.result.length > 0) {
|
|
98
|
+
return lastResultRef.current.result;
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
// Create a stable key for memoization to prevent unnecessary recalculations
|
|
90
102
|
const memoKey = `${viewport.scrollTop}-${
|
|
91
103
|
viewport.containerHeight
|
|
@@ -96,7 +108,7 @@ export const useVisibleNodes = ({
|
|
|
96
108
|
return lastResultRef.current.result;
|
|
97
109
|
}
|
|
98
110
|
|
|
99
|
-
if (table === null
|
|
111
|
+
if (table === null) return [];
|
|
100
112
|
|
|
101
113
|
const visibleRows: number[] = [];
|
|
102
114
|
const {scrollTop, containerHeight} = viewport;
|
|
@@ -104,11 +116,35 @@ export const useVisibleNodes = ({
|
|
|
104
116
|
// Viewport Culling Algorithm:
|
|
105
117
|
// 1. Calculate visible depth range based on scroll position and container height
|
|
106
118
|
// 2. Add 5-row buffer above/below for smooth scrolling experience
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
// Note: We never shrink the rendered range to avoid back and forth node removals (and in turn additions when scrolled down again) to the dom.
|
|
120
|
+
|
|
121
|
+
const BUFFER = 15; // Buffer for smoother scrolling
|
|
122
|
+
|
|
123
|
+
const visibleStartDepth = Math.max(0, Math.floor(scrollTop / RowHeight) - BUFFER);
|
|
124
|
+
const visibleDepths = Math.ceil(containerHeight / RowHeight);
|
|
125
|
+
const visibleEndDepth = Math.min(effectiveDepth, visibleStartDepth + visibleDepths + BUFFER);
|
|
126
|
+
|
|
127
|
+
// Reset range if table changed (new data loaded) as this is new data
|
|
128
|
+
if (renderedRangeRef.current.table !== table) {
|
|
129
|
+
renderedRangeRef.current = {
|
|
130
|
+
minDepth: Infinity,
|
|
131
|
+
maxDepth: -Infinity,
|
|
132
|
+
table: table,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Expand the rendered range (never shrink when scrolling up/down)
|
|
137
|
+
renderedRangeRef.current.minDepth = Math.min(
|
|
138
|
+
renderedRangeRef.current.minDepth,
|
|
139
|
+
visibleStartDepth
|
|
111
140
|
);
|
|
141
|
+
renderedRangeRef.current.maxDepth = Math.max(
|
|
142
|
+
renderedRangeRef.current.maxDepth,
|
|
143
|
+
visibleEndDepth
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const startDepth = renderedRangeRef.current.minDepth;
|
|
147
|
+
const endDepth = renderedRangeRef.current.maxDepth;
|
|
112
148
|
|
|
113
149
|
const cumulativeColumn = table.getChild(FIELD_CUMULATIVE);
|
|
114
150
|
const valueOffsetColumn = table.getChild(FIELD_VALUE_OFFSET);
|
|
@@ -29,7 +29,7 @@ interface MetricsGraphSectionProps {
|
|
|
29
29
|
querySelection: QuerySelection;
|
|
30
30
|
profileSelection: ProfileSelection | null;
|
|
31
31
|
comparing: boolean;
|
|
32
|
-
sumBy: string[] |
|
|
32
|
+
sumBy: string[] | undefined;
|
|
33
33
|
defaultSumByLoading: boolean;
|
|
34
34
|
queryClient: QueryServiceClient;
|
|
35
35
|
queryExpressionString: string;
|
|
@@ -170,7 +170,7 @@ export function MetricsGraphSection({
|
|
|
170
170
|
to={querySelection.to}
|
|
171
171
|
profile={profileSelection}
|
|
172
172
|
comparing={comparing}
|
|
173
|
-
sumBy={
|
|
173
|
+
sumBy={sumBy ?? []}
|
|
174
174
|
sumByLoading={defaultSumByLoading}
|
|
175
175
|
setTimeRange={handleTimeRangeChange}
|
|
176
176
|
addLabelMatcher={addLabelMatcher}
|
|
@@ -209,10 +209,6 @@ const ProfileSelector = ({
|
|
|
209
209
|
const currentTo = timeRangeSelection.getToMs(true);
|
|
210
210
|
const currentRangeKey = timeRangeSelection.getRangeKey();
|
|
211
211
|
// Commit with refreshed time range
|
|
212
|
-
console.log(
|
|
213
|
-
'[draftExpression] setQueryExpression: committing with refreshed time range:',
|
|
214
|
-
draftSelection.expression
|
|
215
|
-
);
|
|
216
212
|
commitDraft({
|
|
217
213
|
from: currentFrom,
|
|
218
214
|
to: currentTo,
|
|
@@ -332,7 +328,7 @@ const ProfileSelector = ({
|
|
|
332
328
|
querySelection={querySelection}
|
|
333
329
|
profileSelection={profileSelection}
|
|
334
330
|
comparing={comparing}
|
|
335
|
-
sumBy={querySelection.sumBy
|
|
331
|
+
sumBy={querySelection.sumBy}
|
|
336
332
|
defaultSumByLoading={sumByLoading}
|
|
337
333
|
queryClient={queryClient}
|
|
338
334
|
queryExpressionString={queryExpressionString}
|
|
@@ -18,6 +18,8 @@ import {useProfileFilters} from '../components/ProfileFilters/useProfileFilters'
|
|
|
18
18
|
export const useResetStateOnProfileTypeChange = (): (() => void) => {
|
|
19
19
|
const [groupBy, setGroupBy] = useURLState('group_by');
|
|
20
20
|
const [curPath, setCurPath] = useURLState('cur_path');
|
|
21
|
+
const [sumByA, setSumByA] = useURLState('sum_by_a');
|
|
22
|
+
const [sumByB, setSumByB] = useURLState('sum_by_b');
|
|
21
23
|
const {resetFilters} = useProfileFilters();
|
|
22
24
|
const [sandwichFunctionName, setSandwichFunctionName] = useURLState('sandwich_function_name');
|
|
23
25
|
const batchUpdates = useURLStateBatch();
|
|
@@ -34,6 +36,12 @@ export const useResetStateOnProfileTypeChange = (): (() => void) => {
|
|
|
34
36
|
if (sandwichFunctionName !== undefined) {
|
|
35
37
|
setSandwichFunctionName(undefined);
|
|
36
38
|
}
|
|
39
|
+
if (sumByA !== undefined) {
|
|
40
|
+
setSumByA(undefined);
|
|
41
|
+
}
|
|
42
|
+
if (sumByB !== undefined) {
|
|
43
|
+
setSumByB(undefined);
|
|
44
|
+
}
|
|
37
45
|
|
|
38
46
|
resetFilters();
|
|
39
47
|
});
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
|
14
|
+
import React, {Fragment, useCallback, useEffect, useRef, useState} from 'react';
|
|
15
15
|
|
|
16
16
|
import {Icon} from '@iconify/react';
|
|
17
17
|
import cx from 'classnames';
|
|
@@ -350,7 +350,7 @@ const CustomSelect: React.FC<CustomSelectProps & Record<string, any>> = ({
|
|
|
350
350
|
</div>
|
|
351
351
|
) : (
|
|
352
352
|
groupedFilteredItems.map(group => (
|
|
353
|
-
|
|
353
|
+
<Fragment key={group.type}>
|
|
354
354
|
{groupedFilteredItems.length > 1 &&
|
|
355
355
|
groupedFilteredItems.every(g => g.type !== '') &&
|
|
356
356
|
group.type !== '' ? (
|
|
@@ -369,7 +369,7 @@ const CustomSelect: React.FC<CustomSelectProps & Record<string, any>> = ({
|
|
|
369
369
|
handleSelection={handleSelection}
|
|
370
370
|
/>
|
|
371
371
|
))}
|
|
372
|
-
|
|
372
|
+
</Fragment>
|
|
373
373
|
))
|
|
374
374
|
)}
|
|
375
375
|
</div>
|