@parca/profile 0.19.25 → 0.19.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.d.ts.map +1 -1
  3. package/dist/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.js +8 -0
  4. package/dist/ProfileFlameGraph/FlameGraphArrow/index.d.ts.map +1 -1
  5. package/dist/ProfileFlameGraph/FlameGraphArrow/index.js +33 -42
  6. package/dist/ProfileFlameGraph/FlameGraphArrow/useScrollViewport.d.ts +8 -0
  7. package/dist/ProfileFlameGraph/FlameGraphArrow/useScrollViewport.d.ts.map +1 -0
  8. package/dist/ProfileFlameGraph/FlameGraphArrow/useScrollViewport.js +70 -0
  9. package/dist/ProfileFlameGraph/FlameGraphArrow/useVisibleNodes.d.ts +24 -0
  10. package/dist/ProfileFlameGraph/FlameGraphArrow/useVisibleNodes.d.ts.map +1 -0
  11. package/dist/ProfileFlameGraph/FlameGraphArrow/useVisibleNodes.js +111 -0
  12. package/dist/ProfileFlameGraph/FlameGraphArrow/utils.d.ts +2 -1
  13. package/dist/ProfileFlameGraph/FlameGraphArrow/utils.d.ts.map +1 -1
  14. package/dist/ProfileFlameGraph/FlameGraphArrow/utils.js +11 -0
  15. package/dist/ProfileView/components/ProfileFilters/filterPresets.d.ts +1 -1
  16. package/dist/ProfileView/components/ProfileFilters/filterPresets.d.ts.map +1 -1
  17. package/dist/ProfileView/components/ProfileFilters/filterPresets.js +2 -1
  18. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts +7 -4
  19. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts.map +1 -1
  20. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.js +39 -50
  21. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts +1 -1
  22. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
  23. package/dist/ProfileView/components/Toolbars/index.js +1 -1
  24. package/dist/ProfileView/components/ViewSelector/index.js +1 -1
  25. package/dist/styles.css +1 -1
  26. package/package.json +5 -5
  27. package/src/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.tsx +214 -200
  28. package/src/ProfileFlameGraph/FlameGraphArrow/index.tsx +90 -90
  29. package/src/ProfileFlameGraph/FlameGraphArrow/useScrollViewport.ts +89 -0
  30. package/src/ProfileFlameGraph/FlameGraphArrow/useVisibleNodes.ts +167 -0
  31. package/src/ProfileFlameGraph/FlameGraphArrow/utils.ts +12 -1
  32. package/src/ProfileView/components/ProfileFilters/filterPresets.ts +4 -2
  33. package/src/ProfileView/components/ProfileFilters/useProfileFilters.ts +57 -76
  34. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +1 -1
  35. package/src/ProfileView/components/Toolbars/index.tsx +1 -1
  36. package/src/ProfileView/components/ViewSelector/index.tsx +1 -1
@@ -77,212 +77,226 @@ export const fadedFlameRectStyles = {
77
77
  opacity: '0.5',
78
78
  };
79
79
 
80
- export const FlameNode = React.memo(function FlameNodeNoMemo({
81
- table,
82
- row,
83
- colors,
84
- colorBy,
85
- height,
86
- totalWidth,
87
- darkMode,
88
- compareMode,
89
- colorForSimilarNodes,
90
- selectedRow,
91
- onClick,
92
- onContextMenu,
93
- hoveringRow,
94
- setHoveringRow,
95
- isFlameChart,
96
- profileSource,
97
- isRenderedAsFlamegraph = false,
98
- isInSandwichView = false,
99
- maxDepth = 0,
100
- effectiveDepth,
101
- tooltipId = 'default',
102
- }: FlameNodeProps): React.JSX.Element {
103
- // get the columns to read from
104
- const mappingColumn = table.getChild(FIELD_MAPPING_FILE);
105
- const functionNameColumn = table.getChild(FIELD_FUNCTION_NAME);
106
- const cumulativeColumn = table.getChild(FIELD_CUMULATIVE);
107
- const depthColumn = table.getChild(FIELD_DEPTH);
108
- const diffColumn = table.getChild(FIELD_DIFF);
109
- const filenameColumn = table.getChild(FIELD_FUNCTION_FILE_NAME);
110
- const valueOffsetColumn = table.getChild(FIELD_VALUE_OFFSET);
111
- const tsColumn = table.getChild(FIELD_TIMESTAMP);
112
-
113
- // get the actual values from the columns
114
- const binaries = useAppSelector(selectBinaries);
115
-
116
- const mappingFile: string | null = arrowToString(mappingColumn?.get(row));
117
- const functionName: string | null = arrowToString(functionNameColumn?.get(row));
118
- const cumulative = cumulativeColumn?.get(row) !== null ? BigInt(cumulativeColumn?.get(row)) : 0n;
119
- const diff: bigint | null = diffColumn?.get(row) !== null ? BigInt(diffColumn?.get(row)) : null;
120
- const filename: string | null = arrowToString(filenameColumn?.get(row));
121
- const depth: number = depthColumn?.get(row) ?? 0;
122
-
123
- const valueOffset: bigint =
124
- valueOffsetColumn?.get(row) !== null && valueOffsetColumn?.get(row) !== undefined
125
- ? BigInt(valueOffsetColumn?.get(row))
126
- : 0n;
127
-
128
- const colorAttribute =
129
- colorBy === 'filename' ? filename : colorBy === 'binary' ? mappingFile : null;
130
-
131
- const colorsMap = colors;
132
-
133
- const hoveringName =
134
- hoveringRow !== undefined ? arrowToString(functionNameColumn?.get(hoveringRow)) : '';
135
- const shouldBeHighlighted =
136
- functionName != null && hoveringName != null && functionName === hoveringName;
137
-
138
- const colorResult = useNodeColor({
139
- isDarkMode: darkMode,
80
+ export const FlameNode = React.memo(
81
+ function FlameNodeNoMemo({
82
+ table,
83
+ row,
84
+ colors,
85
+ colorBy,
86
+ height,
87
+ totalWidth,
88
+ darkMode,
140
89
  compareMode,
141
- cumulative,
142
- diff,
143
- colorsMap,
144
- colorAttribute,
145
- });
146
-
147
- const name = useMemo(() => {
148
- return row === 0 ? 'root' : nodeLabel(table, row, binaries.length > 1);
149
- }, [table, row, binaries]);
150
-
151
- // Hide frames beyond effective depth limit
152
- if (effectiveDepth !== undefined && depth > effectiveDepth) {
153
- return <></>;
154
- }
155
-
156
- const selectionOffset =
157
- valueOffsetColumn?.get(selectedRow) !== null &&
158
- valueOffsetColumn?.get(selectedRow) !== undefined
159
- ? BigInt(valueOffsetColumn?.get(selectedRow))
160
- : 0n;
161
- const selectionCumulative =
162
- cumulativeColumn?.get(selectedRow) !== null ? BigInt(cumulativeColumn?.get(selectedRow)) : 0n;
163
- if (
164
- valueOffset + cumulative <= selectionOffset ||
165
- valueOffset >= selectionOffset + selectionCumulative
166
- ) {
167
- // If the end of the node is before the selection offset or the start of the node is after the selection offset + totalWidth, we don't render it.
168
- return <></>;
169
- }
170
-
171
- if (row === 0 && (isFlameChart || isInSandwichView)) {
172
- // The root node is not rendered in the flame chart or sandwich view, so we return null.
173
- return <></>;
174
- }
175
-
176
- // 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.
177
- const tsBounds = boundsFromProfileSource(profileSource);
178
- const total = cumulativeColumn?.get(selectedRow);
179
- const totalRatio = cumulative > total ? 1 : Number(cumulative) / Number(total);
180
- const width: number = isFlameChart
181
- ? (Number(cumulative) / (Number(tsBounds[1]) - Number(tsBounds[0]))) * totalWidth
182
- : totalRatio * totalWidth;
183
-
184
- if (width <= 1) {
185
- return <></>;
186
- }
90
+ colorForSimilarNodes,
91
+ selectedRow,
92
+ onClick,
93
+ onContextMenu,
94
+ hoveringRow,
95
+ setHoveringRow,
96
+ isFlameChart,
97
+ profileSource,
98
+ isRenderedAsFlamegraph = false,
99
+ isInSandwichView = false,
100
+ maxDepth = 0,
101
+ effectiveDepth,
102
+ tooltipId = 'default',
103
+ }: FlameNodeProps): React.JSX.Element {
104
+ // get the columns to read from
105
+ const mappingColumn = table.getChild(FIELD_MAPPING_FILE);
106
+ const functionNameColumn = table.getChild(FIELD_FUNCTION_NAME);
107
+ const cumulativeColumn = table.getChild(FIELD_CUMULATIVE);
108
+ const depthColumn = table.getChild(FIELD_DEPTH);
109
+ const diffColumn = table.getChild(FIELD_DIFF);
110
+ const filenameColumn = table.getChild(FIELD_FUNCTION_FILE_NAME);
111
+ const valueOffsetColumn = table.getChild(FIELD_VALUE_OFFSET);
112
+ const tsColumn = table.getChild(FIELD_TIMESTAMP);
113
+
114
+ // get the actual values from the columns
115
+ const binaries = useAppSelector(selectBinaries);
116
+
117
+ const mappingFile: string | null = arrowToString(mappingColumn?.get(row));
118
+ const functionName: string | null = arrowToString(functionNameColumn?.get(row));
119
+ const cumulative =
120
+ cumulativeColumn?.get(row) !== null ? BigInt(cumulativeColumn?.get(row)) : 0n;
121
+ const diff: bigint | null = diffColumn?.get(row) !== null ? BigInt(diffColumn?.get(row)) : null;
122
+ const filename: string | null = arrowToString(filenameColumn?.get(row));
123
+ const depth: number = depthColumn?.get(row) ?? 0;
124
+
125
+ const valueOffset: bigint =
126
+ valueOffsetColumn?.get(row) !== null && valueOffsetColumn?.get(row) !== undefined
127
+ ? BigInt(valueOffsetColumn?.get(row))
128
+ : 0n;
129
+
130
+ const colorAttribute =
131
+ colorBy === 'filename' ? filename : colorBy === 'binary' ? mappingFile : null;
132
+
133
+ const colorsMap = colors;
134
+
135
+ const hoveringName =
136
+ hoveringRow !== undefined ? arrowToString(functionNameColumn?.get(hoveringRow)) : '';
137
+ const shouldBeHighlighted =
138
+ functionName != null && hoveringName != null && functionName === hoveringName;
139
+
140
+ const colorResult = useNodeColor({
141
+ isDarkMode: darkMode,
142
+ compareMode,
143
+ cumulative,
144
+ diff,
145
+ colorsMap,
146
+ colorAttribute,
147
+ });
148
+
149
+ const name = useMemo(() => {
150
+ return row === 0 ? 'root' : nodeLabel(table, row, binaries.length > 1);
151
+ }, [table, row, binaries]);
152
+
153
+ // Hide frames beyond effective depth limit
154
+ if (effectiveDepth !== undefined && depth > effectiveDepth) {
155
+ return <></>;
156
+ }
187
157
 
188
- const selectedDepth = depthColumn?.get(selectedRow);
189
- const styles =
190
- selectedDepth !== undefined && selectedDepth > depth ? fadedFlameRectStyles : flameRectStyles;
158
+ const selectionOffset =
159
+ valueOffsetColumn?.get(selectedRow) !== null &&
160
+ valueOffsetColumn?.get(selectedRow) !== undefined
161
+ ? BigInt(valueOffsetColumn?.get(selectedRow))
162
+ : 0n;
163
+ const selectionCumulative =
164
+ cumulativeColumn?.get(selectedRow) !== null ? BigInt(cumulativeColumn?.get(selectedRow)) : 0n;
165
+ if (
166
+ valueOffset + cumulative <= selectionOffset ||
167
+ valueOffset >= selectionOffset + selectionCumulative
168
+ ) {
169
+ // If the end of the node is before the selection offset or the start of the node is after the selection offset + totalWidth, we don't render it.
170
+ return <></>;
171
+ }
191
172
 
192
- const onMouseEnter = (): void => {
193
- setHoveringRow(row);
194
- window.dispatchEvent(
195
- new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
196
- detail: {row},
197
- })
198
- );
199
- };
200
-
201
- const onMouseLeave = (): void => {
202
- setHoveringRow(undefined);
203
- window.dispatchEvent(
204
- new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
205
- detail: {row: null},
206
- })
207
- );
208
- };
209
-
210
- const handleContextMenu = (e: React.MouseEvent): void => {
211
- onContextMenu(e, row);
212
- };
213
-
214
- const ts = tsColumn !== null ? Number(tsColumn.get(row)) : 0;
215
- const x =
216
- isFlameChart && tsColumn !== null
217
- ? ((ts - Number(tsBounds[0])) / (Number(tsBounds[1]) - Number(tsBounds[0]))) * totalWidth
218
- : selectedDepth > depth
219
- ? 0
220
- : ((Number(valueOffset) - Number(selectionOffset)) / Number(total)) * totalWidth;
221
-
222
- const calculateY = (
223
- isRenderedAsFlamegraph: boolean,
224
- isInSandwichView: boolean,
225
- isFlameChart: boolean,
226
- maxDepth: number,
227
- depth: number,
228
- height: number
229
- ): number => {
230
- if (isRenderedAsFlamegraph) {
231
- return (maxDepth - depth) * height; // Flamegraph is inverted
173
+ if (row === 0 && (isFlameChart || isInSandwichView)) {
174
+ // The root node is not rendered in the flame chart or sandwich view, so we return null.
175
+ return <></>;
232
176
  }
233
177
 
234
- if (isFlameChart || isInSandwichView) {
235
- return (depth - 1) * height;
178
+ // 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.
179
+ const tsBounds = boundsFromProfileSource(profileSource);
180
+ const total = cumulativeColumn?.get(selectedRow);
181
+ const totalRatio = cumulative > total ? 1 : Number(cumulative) / Number(total);
182
+ const width: number = isFlameChart
183
+ ? (Number(cumulative) / (Number(tsBounds[1]) - Number(tsBounds[0]))) * totalWidth
184
+ : totalRatio * totalWidth;
185
+
186
+ if (width <= 1) {
187
+ return <></>;
236
188
  }
237
189
 
238
- return depth * height;
239
- };
190
+ const selectedDepth = depthColumn?.get(selectedRow);
191
+ const styles =
192
+ selectedDepth !== undefined && selectedDepth > depth ? fadedFlameRectStyles : flameRectStyles;
193
+
194
+ const onMouseEnter = (): void => {
195
+ setHoveringRow(row);
196
+ window.dispatchEvent(
197
+ new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
198
+ detail: {row},
199
+ })
200
+ );
201
+ };
202
+
203
+ const onMouseLeave = (): void => {
204
+ setHoveringRow(undefined);
205
+ window.dispatchEvent(
206
+ new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
207
+ detail: {row: null},
208
+ })
209
+ );
210
+ };
211
+
212
+ const handleContextMenu = (e: React.MouseEvent): void => {
213
+ onContextMenu(e, row);
214
+ };
215
+
216
+ const ts = tsColumn !== null ? Number(tsColumn.get(row)) : 0;
217
+ const x =
218
+ isFlameChart && tsColumn !== null
219
+ ? ((ts - Number(tsBounds[0])) / (Number(tsBounds[1]) - Number(tsBounds[0]))) * totalWidth
220
+ : selectedDepth > depth
221
+ ? 0
222
+ : ((Number(valueOffset) - Number(selectionOffset)) / Number(total)) * totalWidth;
223
+
224
+ const calculateY = (
225
+ isRenderedAsFlamegraph: boolean,
226
+ isInSandwichView: boolean,
227
+ isFlameChart: boolean,
228
+ maxDepth: number,
229
+ depth: number,
230
+ height: number
231
+ ): number => {
232
+ if (isRenderedAsFlamegraph) {
233
+ return (maxDepth - depth) * height; // Flamegraph is inverted
234
+ }
235
+
236
+ if (isFlameChart || isInSandwichView) {
237
+ return (depth - 1) * height;
238
+ }
239
+
240
+ return depth * height;
241
+ };
242
+
243
+ const y = calculateY(
244
+ isRenderedAsFlamegraph,
245
+ isInSandwichView,
246
+ isFlameChart,
247
+ effectiveDepth ?? maxDepth,
248
+ depth,
249
+ height
250
+ );
240
251
 
241
- const y = calculateY(
242
- isRenderedAsFlamegraph,
243
- isInSandwichView,
244
- isFlameChart,
245
- effectiveDepth ?? maxDepth,
246
- depth,
247
- height
248
- );
249
-
250
- return (
251
- <>
252
- <g
253
- id={row === 0 ? 'root-span' : undefined}
254
- transform={`translate(${x + 1}, ${y + 1})`}
255
- style={styles}
256
- onMouseEnter={onMouseEnter}
257
- onMouseLeave={onMouseLeave}
258
- onClick={onClick}
259
- onContextMenu={handleContextMenu}
260
- >
261
- <rect
262
- x={0}
263
- y={0}
264
- width={width}
265
- height={height}
266
- style={{
267
- fill: colorResult,
268
- }}
269
- className={cx(
270
- shouldBeHighlighted
271
- ? `${colorForSimilarNodes} stroke-[3] [stroke-dasharray:6,4] [stroke-linecap:round] [stroke-linejoin:round] h-6`
272
- : 'stroke-white dark:stroke-gray-700'
252
+ return (
253
+ <>
254
+ <g
255
+ id={row === 0 ? 'root-span' : undefined}
256
+ transform={`translate(${x + 1}, ${y + 1})`}
257
+ style={styles}
258
+ onMouseEnter={onMouseEnter}
259
+ onMouseLeave={onMouseLeave}
260
+ onClick={onClick}
261
+ onContextMenu={handleContextMenu}
262
+ >
263
+ <rect
264
+ x={0}
265
+ y={0}
266
+ width={width}
267
+ height={height}
268
+ style={{
269
+ fill: colorResult,
270
+ }}
271
+ className={cx(
272
+ shouldBeHighlighted
273
+ ? `${colorForSimilarNodes} stroke-[3] [stroke-dasharray:6,4] [stroke-linecap:round] [stroke-linejoin:round] h-6`
274
+ : 'stroke-white dark:stroke-gray-700'
275
+ )}
276
+ />
277
+ {width > 5 && (
278
+ <svg width={width - 5} height={height}>
279
+ <TextWithEllipsis
280
+ text={name}
281
+ x={5}
282
+ y={15}
283
+ width={width - 10} // Subtract padding from available width
284
+ />
285
+ </svg>
273
286
  )}
274
- />
275
- {width > 5 && (
276
- <svg width={width - 5} height={height}>
277
- <TextWithEllipsis
278
- text={name}
279
- x={5}
280
- y={15}
281
- width={width - 10} // Subtract padding from available width
282
- />
283
- </svg>
284
- )}
285
- </g>
286
- </>
287
- );
288
- });
287
+ </g>
288
+ </>
289
+ );
290
+ },
291
+ (prevProps, nextProps) => {
292
+ // Only re-render if the relevant props have changed
293
+ return (
294
+ prevProps.row === nextProps.row &&
295
+ prevProps.selectedRow === nextProps.selectedRow &&
296
+ prevProps.hoveringRow === nextProps.hoveringRow &&
297
+ prevProps.totalWidth === nextProps.totalWidth &&
298
+ prevProps.height === nextProps.height &&
299
+ prevProps.effectiveDepth === nextProps.effectiveDepth
300
+ );
301
+ }
302
+ );
@@ -39,12 +39,15 @@ import {FlameNode, RowHeight, colorByColors} from './FlameGraphNodes';
39
39
  import {MemoizedTooltip} from './MemoizedTooltip';
40
40
  import {TooltipProvider} from './TooltipContext';
41
41
  import {useFilenamesList} from './useMappingList';
42
+ import {useScrollViewport} from './useScrollViewport';
43
+ import {useVisibleNodes} from './useVisibleNodes';
42
44
  import {
43
45
  CurrentPathFrame,
44
46
  arrowToString,
45
47
  extractFeature,
46
48
  extractFilenameFeature,
47
49
  getCurrentPathFrameData,
50
+ getMaxDepth,
48
51
  isCurrentPathFrameMatch,
49
52
  } from './utils';
50
53
 
@@ -120,17 +123,6 @@ export const getFilenameColors = (
120
123
 
121
124
  const noop = (): void => {};
122
125
 
123
- function getMaxDepth(depthColumn: Vector<any> | null): number {
124
- if (depthColumn === null) return 0;
125
-
126
- let max = 0;
127
- for (const val of depthColumn) {
128
- const numVal = Number(val);
129
- if (numVal > max) max = numVal;
130
- }
131
- return max;
132
- }
133
-
134
126
  export const FlameGraphArrow = memo(function FlameGraphArrow({
135
127
  arrow,
136
128
  total,
@@ -166,35 +158,11 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
166
158
  return result;
167
159
  }, [arrow, perf]);
168
160
  const svg = useRef(null);
161
+ const containerRef = useRef<HTMLDivElement>(null);
169
162
  const renderStartTime = useRef<number>(0);
170
163
 
171
164
  const [svgElement, setSvgElement] = useState<SVGSVGElement | null>(null);
172
165
 
173
- useEffect(() => {
174
- if (perf?.markInteraction != null) {
175
- renderStartTime.current = performance.now();
176
- }
177
- }, [table, width, curPath, perf]);
178
-
179
- useEffect(() => {
180
- if (perf?.setMeasurement != null && renderStartTime.current > 0) {
181
- const measureRenderTime = (): void => {
182
- const renderTime = performance.now() - renderStartTime.current;
183
- if (perf?.setMeasurement != null) {
184
- perf.setMeasurement('flamegraph.render_time', renderTime);
185
- }
186
-
187
- renderStartTime.current = 0;
188
- };
189
-
190
- requestAnimationFrame(measureRenderTime);
191
- }
192
- }, [table, width, curPath, perf]);
193
-
194
- useEffect(() => {
195
- setSvgElement(svg.current);
196
- }, [tooltipId]);
197
-
198
166
  const {excludeBinary} = useProfileFilters();
199
167
 
200
168
  const {compareMode} = useProfileViewContext();
@@ -279,20 +247,27 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
279
247
  excludeBinary(binaryToRemove);
280
248
  };
281
249
 
282
- const handleRowClick = (row: number): void => {
283
- // Walk down the stack starting at row until we reach the root (row 0).
284
- const path: CurrentPathFrame[] = [];
285
- let currentRow = row;
286
- while (currentRow > 0) {
287
- const frame = getCurrentPathFrameData(table, currentRow);
288
- path.push(frame);
289
- currentRow = table.getChild(FIELD_PARENT)?.get(currentRow) ?? 0;
290
- }
291
-
292
- // Reverse the path so that the root is first.
293
- path.reverse();
294
- setCurPath(path);
295
- };
250
+ const handleRowClick = useCallback(
251
+ (row: number): void => {
252
+ if (isFlameChart) {
253
+ // In flame charts, we don't want to expand the node, so we return early.
254
+ return;
255
+ }
256
+ // Walk down the stack starting at row until we reach the root (row 0).
257
+ const path: CurrentPathFrame[] = [];
258
+ let currentRow = row;
259
+ while (currentRow > 0) {
260
+ const frame = getCurrentPathFrameData(table, currentRow);
261
+ path.push(frame);
262
+ currentRow = table.getChild(FIELD_PARENT)?.get(currentRow) ?? 0;
263
+ }
264
+
265
+ // Reverse the path so that the root is first.
266
+ path.reverse();
267
+ setCurPath(path);
268
+ },
269
+ [table, setCurPath, isFlameChart]
270
+ );
296
271
 
297
272
  const depthColumn = table.getChild(FIELD_DEPTH);
298
273
  const maxDepth = getMaxDepth(depthColumn);
@@ -304,10 +279,13 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
304
279
  // Use deferred value to prevent UI blocking when expanding frames
305
280
  const deferredEffectiveDepth = useDeferredValue(effectiveDepth);
306
281
 
307
- const height = isInSandwichView
282
+ const totalHeight = isInSandwichView
308
283
  ? deferredEffectiveDepth * RowHeight
309
284
  : (deferredEffectiveDepth + 1) * RowHeight;
310
285
 
286
+ // Get the viewport of the container, this is used to determine which rows are visible.
287
+ const viewport = useScrollViewport(containerRef);
288
+
311
289
  // To find the selected row, we must walk the current path and look at which
312
290
  // children of the current frame matches the path element exactly. Until the
313
291
  // end, the row we find at the end is our selected row.
@@ -333,6 +311,25 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
333
311
  }
334
312
  const selectedRow = currentRow;
335
313
 
314
+ const visibleNodes = useVisibleNodes({
315
+ table,
316
+ viewport,
317
+ total,
318
+ width: width ?? 1,
319
+ selectedRow,
320
+ effectiveDepth: deferredEffectiveDepth,
321
+ });
322
+
323
+ useEffect(() => {
324
+ if (perf?.markInteraction != null) {
325
+ renderStartTime.current = performance.now();
326
+ }
327
+ }, [table, width, curPath, perf]);
328
+
329
+ useEffect(() => {
330
+ setSvgElement(svg.current);
331
+ }, [tooltipId]);
332
+
336
333
  return (
337
334
  <TooltipProvider
338
335
  table={table}
@@ -359,46 +356,49 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
359
356
  isInSandwichView={isInSandwichView}
360
357
  />
361
358
  <MemoizedTooltip contextElement={svgElement} dockedMetainfo={dockedMetainfo} />
362
- <svg
363
- className="font-robotoMono"
364
- width={width}
365
- height={height}
366
- preserveAspectRatio="xMinYMid"
367
- ref={svg}
359
+ <div
360
+ ref={containerRef}
361
+ 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"
362
+ style={{
363
+ width: width ?? '100%',
364
+ contain: 'layout style paint',
365
+ }}
368
366
  >
369
- {Array.from({length: table.numRows}, (_, row) => (
370
- <FlameNode
371
- key={row}
372
- table={table}
373
- row={row} // root is always row 0 in the arrow record
374
- colors={colorByColors}
375
- colorBy={colorByValue}
376
- totalWidth={width ?? 1}
377
- height={RowHeight}
378
- darkMode={isDarkMode}
379
- compareMode={compareMode}
380
- colorForSimilarNodes={colorForSimilarNodes}
381
- selectedRow={selectedRow}
382
- onClick={() => {
383
- if (isFlameChart) {
384
- // We don't want to expand in flame charts.
385
- return;
386
- }
387
- handleRowClick(row);
388
- }}
389
- onContextMenu={displayMenu}
390
- hoveringRow={highlightSimilarStacksPreference ? hoveringRow : undefined}
391
- setHoveringRow={highlightSimilarStacksPreference ? setHoveringRow : noop}
392
- isFlameChart={isFlameChart}
393
- profileSource={profileSource}
394
- isRenderedAsFlamegraph={isRenderedAsFlamegraph}
395
- isInSandwichView={isInSandwichView}
396
- maxDepth={maxDepth}
397
- effectiveDepth={deferredEffectiveDepth}
398
- tooltipId={tooltipId}
399
- />
400
- ))}
401
- </svg>
367
+ <svg
368
+ className="font-robotoMono"
369
+ width={width ?? 0}
370
+ height={totalHeight}
371
+ preserveAspectRatio="xMinYMid"
372
+ ref={svg}
373
+ >
374
+ {visibleNodes.map(row => (
375
+ <FlameNode
376
+ key={row}
377
+ table={table}
378
+ row={row}
379
+ colors={colorByColors}
380
+ colorBy={colorByValue}
381
+ totalWidth={width ?? 1}
382
+ height={RowHeight}
383
+ darkMode={isDarkMode}
384
+ compareMode={compareMode}
385
+ colorForSimilarNodes={colorForSimilarNodes}
386
+ selectedRow={selectedRow}
387
+ onClick={() => handleRowClick(row)}
388
+ onContextMenu={displayMenu}
389
+ hoveringRow={highlightSimilarStacksPreference ? hoveringRow : undefined}
390
+ setHoveringRow={highlightSimilarStacksPreference ? setHoveringRow : noop}
391
+ isFlameChart={isFlameChart}
392
+ profileSource={profileSource}
393
+ isRenderedAsFlamegraph={isRenderedAsFlamegraph}
394
+ isInSandwichView={isInSandwichView}
395
+ maxDepth={maxDepth}
396
+ effectiveDepth={deferredEffectiveDepth}
397
+ tooltipId={tooltipId}
398
+ />
399
+ ))}
400
+ </svg>
401
+ </div>
402
402
  </div>
403
403
  </TooltipProvider>
404
404
  );