fix(webui): Fix more memory leaks#1765
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR hardens WebUI lifecycle behavior by implementing consistent cleanup patterns: tracking async work ownership (timers, RAF, observers), guarding against detached DOM operations, preventing state updates after unmount, and safely removing DOM-mounted elements. The changes span shared libs (observer pruning, portal cleanup, scroll state), the preview popup system (anchor tracking, session auto-close on disconnect), and timeline/rendering infrastructure (unmount guards, safe mount/remove helpers, timer tracking). ChangesDOM & Lifecycle Cleanup Infrastructure
Preview Popup Session Lifecycle Management
Timeline & Renderer Lifecycle Management
Event Handler Memoization & Listener Lifecycle
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
8e448db to
fe66f7f
Compare
fe66f7f to
99fd994
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
packages/webui/src/client/lib/viewPort.ts (1)
258-265:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRelease
currentScrollingElementon the non-second-stage exits.Line 263 stores a live segment element, but the first-stage success path and the no-scroll return never clear it. If that segment is later detached, the module scope still keeps the node reachable until another scroll or lifecycle reset happens.
Suggested fix
if (!secondStage) { // Wait to settle 1 atemt to scroll pendingFirstStageTimeout = setTimeout(() => { pendingFirstStageTimeout = undefined let { top, bottom } = elementToScrollTo.getBoundingClientRect() top = Math.floor(top) bottom = Math.floor(bottom) if (bottom > Math.floor(window.innerHeight) || top < headerHeight) { // If not in place atempt to scroll again innerScrollToSegment(elementToScrollTo, forceScroll, true, true).then(resolve, reject) } else { + currentScrollingElement = undefined resolve(true) } }, 1000) // When UI is getting optimized further we could lower this value } else { currentScrollingElement = undefined resolve(true) } }) }, @@ } + currentScrollingElement = undefined return Promise.resolve(false) }Also applies to: 287-299, 315-315
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/webui/src/client/lib/viewPort.ts` around lines 258 - 265, currentScrollingElement is being retained on non-second-stage exits which keeps DOM nodes reachable; change the logic so non-second-stage paths release the reference by setting currentScrollingElement = undefined instead of storing elementToScrollTo, and ensure you also clear currentScrollingElement before any early/no-scroll returns and in the similar blocks referenced (the branches around pendingFirstStageTimeout, the block covering lines ~287-299, and the single-line exit near 315). Update places that set currentScrollingElement to elementToScrollTo (and any early-return paths) to clear it (use currentScrollingElement = undefined) and keep the second-stage behavior unchanged.packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx (1)
253-260:⚠️ Potential issue | 🟡 MinorAdd
togglePreviewPopUpto the mini-inspector callback dependency arrays
toggleMiniInspectorOn/toggleMiniInspectorOffcalltogglePreviewPopUpbut don’t depend on it, so they can retain a staletogglePreviewPopUpclosure when its own dependencies change (also applies to ~333-342).Suggested fix
const toggleMiniInspectorOn = useCallback( (e: React.MouseEvent) => togglePreviewPopUp(e, true), - [piece, cursorTimePosition, contentStatus, timeScale] + [togglePreviewPopUp] ) const toggleMiniInspectorOff = useCallback( (e: React.MouseEvent) => togglePreviewPopUp(e, false), - [piece, cursorTimePosition, contentStatus, timeScale] + [togglePreviewPopUp] )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx` around lines 253 - 260, The two callbacks toggleMiniInspectorOn and toggleMiniInspectorOff call togglePreviewPopUp but don't list it in their dependency arrays, which can lead to stale closures; update both useCallback dependency arrays to include togglePreviewPopUp, and also add togglePreviewPopUp to the dependency arrays of the other callbacks in the same area (~lines 333-342) that invoke it so they always capture the latest togglePreviewPopUp reference.packages/webui/src/client/ui/SegmentTimeline/TimelineGrid.tsx (1)
444-452:⚠️ Potential issue | 🟡 MinorCancel
contextResizethrottling during unmount
contextResizeis a_.throttlethat can execute a trailing call aftercomponentWillUnmount, invokingrepaint()/props.onResize(). Addthis.contextResize.cancel()(same.cancel()pattern already used for other_.throttlehandlers in this repo).Suggested fix
componentWillUnmount(): void { if (typeof this.scheduledRepaint === 'number') { window.cancelAnimationFrame(this.scheduledRepaint) this.scheduledRepaint = null } + this.contextResize.cancel() if (this._resizeObserver) this._resizeObserver.disconnect() window.removeEventListener(RundownTiming.Events.timeupdateLowResolution, this.onTimeupdate) window.removeEventListener(RundownTiming.Events.timeupdateHighResolution, this.onTimeupdate) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/webui/src/client/ui/SegmentTimeline/TimelineGrid.tsx` around lines 444 - 452, componentWillUnmount must cancel the _.throttle stored on this.contextResize to prevent its trailing invocation after unmount (which can call repaint() / props.onResize()); update componentWillUnmount to call this.contextResize.cancel() (following the same pattern used for other throttled handlers) before unsetting observers/listeners so any queued trailing call is cleared.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/webui/src/client/lib/Escape.tsx`:
- Around line 55-58: The cleanup currently sets rootElemRef.current = null which
breaks portals across id changes; update the cleanup inside the effect that uses
rootElemRef and parentCreatedByThisHook so it removes the element from the DOM
but does NOT null out rootElemRef.current on dependency changes, and instead
perform nulling/removal only on final unmount (e.g. move the nulling into a
separate useEffect with empty deps or guard it with a check that confirms the
component is unmounting), keeping rootElemRef alive across id changes so the new
effect can reuse the existing node; change references in Escape.tsx
(rootElemRef, removeElement, parentCreatedByThisHook, parentElem) accordingly.
In `@packages/webui/src/client/lib/viewPort.ts`:
- Around line 52-54: The code sets
viewPortScrollingState.isProgrammaticScrollInProgress and focusState.startTime
before knowing whether scrollToPartInstance will actually move the viewport;
move the mutation so it only happens after you determine a non-zero scroll will
occur and immediately before starting the smooth-scroll path. Specifically, in
the scroll-to logic (where focusState.startTime,
viewPortScrollingState.isProgrammaticScrollInProgress, and
viewPortScrollingState.lastProgrammaticScrollTime are currently set), check
whether the target requires movement, and only then set
isProgrammaticScrollInProgress and lastProgrammaticScrollTime; in the
already-visible and noAnimation/instant paths do not set the flag (or clear it
synchronously) so getViewPortScrollingState()/VirtualElement.isScrolling() does
not incorrectly report an active programmatic scroll.
---
Outside diff comments:
In `@packages/webui/src/client/lib/viewPort.ts`:
- Around line 258-265: currentScrollingElement is being retained on
non-second-stage exits which keeps DOM nodes reachable; change the logic so
non-second-stage paths release the reference by setting currentScrollingElement
= undefined instead of storing elementToScrollTo, and ensure you also clear
currentScrollingElement before any early/no-scroll returns and in the similar
blocks referenced (the branches around pendingFirstStageTimeout, the block
covering lines ~287-299, and the single-line exit near 315). Update places that
set currentScrollingElement to elementToScrollTo (and any early-return paths) to
clear it (use currentScrollingElement = undefined) and keep the second-stage
behavior unchanged.
In `@packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx`:
- Around line 253-260: The two callbacks toggleMiniInspectorOn and
toggleMiniInspectorOff call togglePreviewPopUp but don't list it in their
dependency arrays, which can lead to stale closures; update both useCallback
dependency arrays to include togglePreviewPopUp, and also add togglePreviewPopUp
to the dependency arrays of the other callbacks in the same area (~lines
333-342) that invoke it so they always capture the latest togglePreviewPopUp
reference.
In `@packages/webui/src/client/ui/SegmentTimeline/TimelineGrid.tsx`:
- Around line 444-452: componentWillUnmount must cancel the _.throttle stored on
this.contextResize to prevent its trailing invocation after unmount (which can
call repaint() / props.onResize()); update componentWillUnmount to call
this.contextResize.cancel() (following the same pattern used for other throttled
handlers) before unsetting observers/listeners so any queued trailing call is
cleared.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2a13e384-1d7d-4797-8947-07c11ea4849c
📒 Files selected for processing (30)
packages/webui/src/client/lib/Escape.tsxpackages/webui/src/client/lib/VirtualElement.tsxpackages/webui/src/client/lib/viewPort.tspackages/webui/src/client/ui/ClockView/CameraScreen/index.tsxpackages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsxpackages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsxpackages/webui/src/client/ui/RundownView.tsxpackages/webui/src/client/ui/SegmentAdlibTesting/SegmentAdlibTesting.tsxpackages/webui/src/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsxpackages/webui/src/client/ui/SegmentList/LinePartPieceIndicator/LinePartAdLibIndicator.tsxpackages/webui/src/client/ui/SegmentList/LinePartPieceIndicator/LinePartIndicator.tsxpackages/webui/src/client/ui/SegmentList/LinePartPieceIndicator/LinePartScriptPiece.tsxpackages/webui/src/client/ui/SegmentList/LinePartPieceIndicator/PieceIndicatorMenu.tsxpackages/webui/src/client/ui/SegmentList/LinePartSecondaryPiece/LinePartSecondaryPiece.tsxpackages/webui/src/client/ui/SegmentList/SegmentList.tsxpackages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsxpackages/webui/src/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsxpackages/webui/src/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnailInner.tsxpackages/webui/src/client/ui/SegmentTimeline/Parts/InvalidPartCover.tsxpackages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsxpackages/webui/src/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsxpackages/webui/src/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsxpackages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsxpackages/webui/src/client/ui/SegmentTimeline/SegmentTimelineContainer.tsxpackages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsxpackages/webui/src/client/ui/SegmentTimeline/TimelineGrid.tsxpackages/webui/src/client/ui/Shelf/DashboardPieceButton/usePreviewPopUpSession.tspackages/webui/src/client/ui/Shelf/Renderers/L3rdListItemRenderer.tsxpackages/webui/src/client/ui/Shelf/Renderers/VTListItemRenderer.tsxpackages/webui/src/client/ui/util/useSetDocumentClass.ts
💤 Files with no reviewable changes (1)
- packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Further testing suggests improvement so will look to merge this tomorrow as no one has commented |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsx`:
- Around line 72-75: The ternary expression in the initialVirtualX assignment is
failing lint checks due to improper formatting of the condition. Wrap the
conditional expression (trackMouse && typeof initialOffsetX === 'number') in
parentheses to provide proper grouping that complies with the formatter
requirements. This ensures the ternary operator and its operands are correctly
grouped for the linter.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 65ec20a2-8782-4b15-a46c-90bfd9a767b4
📒 Files selected for processing (7)
packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsxpackages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsxpackages/webui/src/client/ui/RundownView.tsxpackages/webui/src/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsxpackages/webui/src/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsxpackages/webui/src/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnailInner.tsxpackages/webui/src/client/ui/Shelf/DashboardPieceButton/usePreviewPopUpSession.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- packages/webui/src/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsx
- packages/webui/src/client/ui/Shelf/DashboardPieceButton/usePreviewPopUpSession.ts
- packages/webui/src/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnailInner.tsx
- packages/webui/src/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx
- packages/webui/src/client/ui/RundownView.tsx
- packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx
About the Contributor
This pull request is posted on behalf of the BBC.
Type of Contribution
This is a:
Bug fix
Current Behaviour
UI eventually crashes due to running out of memory.
After running some long term observations / capturing heap snapshots etc. it appears that rundown segments can begin to pile up over time and don't get detached properly.
New Behaviour
Fewer detached DOM elements, there's still more than I would like but I'm not really sure how to proceed at this point as I can't tell where they're still being referenced.
Testing
Affected areas
This PR affects the webui
Time Frame
Not urgent, but we would like to get this merged into the in-development release.
Other Information
As far as I can tell the UI still works as expected and seems slightly more responsive, I imagine time will tell if this PR has made any different or not.
Copilot summarised this as:
Status