Skip to content

Commit 346d314

Browse files
committed
fix(webui): Fix more memory leaks [copilot]
1 parent 3826017 commit 346d314

5 files changed

Lines changed: 122 additions & 67 deletions

File tree

packages/webui/src/client/lib/VirtualElement.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export function VirtualElement({
199199
}
200200
}
201201
} else {
202-
if (ref && isCurrentlyObserving.current) {
202+
if (ref) {
203203
resizeObserverManager.unobserve(ref)
204204
isCurrentlyObserving.current = false
205205
}
@@ -247,7 +247,7 @@ export function VirtualElement({
247247
// Cleanup function
248248
return () => {
249249
// Clean up resize observer
250-
if (ref && isCurrentlyObserving.current) {
250+
if (ref) {
251251
resizeObserverManager.unobserve(ref)
252252
isCurrentlyObserving.current = false
253253
}
@@ -481,6 +481,11 @@ export class ElementObserverManager {
481481

482482
// Disconnect and reconnect mutation observer to refresh the list of observed elements
483483
this.mutationObserver.disconnect()
484+
if (this.observedElements.size === 0) {
485+
this.resizeObserver.disconnect()
486+
return
487+
}
488+
484489
this.observedElements.forEach((_, el) => {
485490
this.mutationObserver.observe(el, {
486491
childList: true,

packages/webui/src/client/lib/viewPort.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ const viewPortScrollingState = {
2222
lastProgrammaticScrollTime: 0,
2323
}
2424

25+
function clearPendingScrollState(): void {
26+
if (pendingFirstStageTimeout) {
27+
clearTimeout(pendingFirstStageTimeout)
28+
pendingFirstStageTimeout = undefined
29+
}
30+
31+
if (pendingSecondStageScroll) {
32+
window.cancelIdleCallback(pendingSecondStageScroll)
33+
pendingSecondStageScroll = undefined
34+
}
35+
36+
currentScrollingElement = undefined
37+
}
38+
2539
export function getViewPortScrollingState(): {
2640
isProgrammaticScrollInProgress: boolean
2741
lastProgrammaticScrollTime: number
@@ -93,6 +107,12 @@ function quitFocusOnPart() {
93107
}
94108
}
95109

110+
export function resetViewportScrollState(): void {
111+
quitFocusOnPart()
112+
clearPendingScrollState()
113+
viewPortScrollingState.isProgrammaticScrollInProgress = false
114+
}
115+
96116
export async function scrollToPartInstance(
97117
partInstanceId: PartInstanceId,
98118
forceScroll?: boolean,
@@ -156,6 +176,8 @@ export async function scrollToSegment(
156176
forceScroll?: boolean,
157177
noAnimation?: boolean
158178
): Promise<boolean> {
179+
clearPendingScrollState()
180+
159181
const elementToScrollTo: HTMLElement | null = getElementToScrollTo(elementToScrollToOrSegmentId, false)
160182
const historyTarget: HTMLElement | null = getElementToScrollTo(elementToScrollToOrSegmentId, true)
161183

@@ -268,6 +290,7 @@ async function innerScrollToSegment(
268290
},
269291
(error) => {
270292
if (!error.toString().match(/another scroll/)) logger.error(error)
293+
currentScrollingElement = undefined
271294
return false
272295
}
273296
)

packages/webui/src/client/ui/RundownView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
maintainFocusOnPartInstance,
3131
scrollToPartInstance,
3232
getHeaderHeight,
33+
resetViewportScrollState,
3334
} from '../lib/viewPort.js'
3435
import { AfterBroadcastForm } from './AfterBroadcastForm.js'
3536
import { RundownRightHandControls } from './RundownView/RundownRightHandControls.js'
@@ -609,6 +610,7 @@ const RundownViewContent = translateWithTracker<IPropsWithReady & ITrackedProps,
609610
document.body.classList.remove('dark', 'vertical-overflow-only')
610611
document.documentElement.removeAttribute('data-bs-theme')
611612
window.removeEventListener('beforeunload', this.onBeforeUnload)
613+
resetViewportScrollState()
612614

613615
documentTitle.set(null)
614616

packages/webui/src/client/ui/SegmentTimeline/Renderers/MicSourceRenderer.tsx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,29 @@ export const MicSourceRenderer: React.ComponentType<IProps> = withTranslation()(
3030
super(props)
3131
}
3232

33+
private mountLineItem(target: HTMLElement | null): void {
34+
if (!this.lineItem || !target) return
35+
36+
if (this.lineItem.parentElement !== target) {
37+
try {
38+
this.lineItem.remove()
39+
} catch (err) {
40+
logger.error(err)
41+
}
42+
target.appendChild(this.lineItem)
43+
}
44+
}
45+
46+
private removeLineItem(): void {
47+
if (!this.lineItem) return
48+
49+
try {
50+
this.lineItem.remove()
51+
} catch (err) {
52+
logger.error(err)
53+
}
54+
}
55+
3356
repositionLine = () => {
3457
if (!this.lineItem) return
3558
this.lineItem.style.left = this.linePosition + 'px'
@@ -103,7 +126,7 @@ export const MicSourceRenderer: React.ComponentType<IProps> = withTranslation()(
103126
this.updateAnchoredElsWidths()
104127
if (this.props.itemElement) {
105128
this.itemElement = this.props.itemElement
106-
this.itemElement.parentElement?.parentElement?.parentElement?.appendChild(this.lineItem)
129+
this.mountLineItem(this.itemElement.parentElement?.parentElement?.parentElement ?? null)
107130
this.refreshLine()
108131
}
109132
}
@@ -148,15 +171,11 @@ export const MicSourceRenderer: React.ComponentType<IProps> = withTranslation()(
148171
// Move the line element
149172
if (this.itemElement !== this.props.itemElement) {
150173
if (this.itemElement && this.lineItem) {
151-
try {
152-
this.lineItem.remove()
153-
} catch (err) {
154-
logger.error(err)
155-
}
174+
this.removeLineItem()
156175
}
157176
this.itemElement = this.props.itemElement
158177
if (this.itemElement && this.lineItem) {
159-
this.itemElement.parentElement?.parentElement?.parentElement?.appendChild(this.lineItem)
178+
this.mountLineItem(this.itemElement.parentElement?.parentElement?.parentElement ?? null)
160179
_forceSizingRecheck = true
161180
}
162181
}
@@ -176,12 +195,9 @@ export const MicSourceRenderer: React.ComponentType<IProps> = withTranslation()(
176195
}
177196

178197
componentWillUnmount(): void {
179-
try {
180-
// Remove the line element
181-
this.lineItem?.remove()
182-
} catch (err) {
183-
logger.error(err)
184-
}
198+
this.removeLineItem()
199+
this.lineItem = null
200+
this.itemElement = null
185201
}
186202

187203
render(): JSX.Element {

packages/webui/src/client/ui/SegmentTimeline/Renderers/VTSourceRenderer.tsx

Lines changed: 61 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,46 @@ class VTSourceRendererBase extends CustomLayerItemRenderer<IProps & WithTranslat
6969
this.rightLabel = e
7070
}
7171

72+
private removeAuxiliaryNode(node: HTMLSpanElement | null): void {
73+
if (!node) return
74+
75+
try {
76+
node.remove()
77+
} catch (err) {
78+
logger.error(`Error in VTSourceRendererBase.removeAuxiliaryNode: ${stringifyError(err)}`)
79+
}
80+
}
81+
82+
private mountAuxiliaryNode(node: HTMLSpanElement | null, target: HTMLElement | null): void {
83+
if (!node || !target) return
84+
85+
if (node.parentElement !== target) {
86+
this.removeAuxiliaryNode(node)
87+
target.appendChild(node)
88+
}
89+
}
90+
91+
private getRightLabelTarget(itemElement: HTMLElement | null): HTMLElement | null {
92+
if (!itemElement) return null
93+
94+
if (this.getItemDuration(true) === Number.POSITIVE_INFINITY) {
95+
return itemElement.parentElement?.parentElement?.parentElement ?? null
96+
}
97+
98+
return itemElement
99+
}
100+
101+
private getCountdownTarget(itemElement: HTMLElement | null): HTMLElement | null {
102+
if (!itemElement) return null
103+
104+
105+
const liveLine =
106+
itemElement.parentElement?.parentElement?.parentElement?.parentElement?.parentElement?.querySelector(
107+
'.segment-timeline__liveline'
108+
)
109+
return liveLine instanceof HTMLElement ? liveLine : null
110+
}
111+
72112
getItemLabelOffsetRight(): React.CSSProperties {
73113
return {
74114
...super.getItemLabelOffsetRight(),
@@ -84,34 +124,23 @@ class VTSourceRendererBase extends CustomLayerItemRenderer<IProps & WithTranslat
84124
newState: Partial<IState>,
85125
itemElement: HTMLElement | null
86126
): Partial<IState> {
87-
if (this.rightLabelContainer && itemElement) {
127+
if (this.rightLabelContainer) {
88128
const itemDuration = this.getItemDuration(true)
129+
const targetElement = this.getRightLabelTarget(itemElement)
89130
if (prevProps === null || itemElement !== prevProps.itemElement) {
90-
if (itemDuration === Number.POSITIVE_INFINITY) {
91-
itemElement.parentElement?.parentElement?.parentElement?.appendChild(this.rightLabelContainer)
92-
93-
newState.rightLabelIsAppendage = true
131+
if (targetElement) {
132+
this.mountAuxiliaryNode(this.rightLabelContainer, targetElement)
133+
newState.rightLabelIsAppendage = itemDuration === Number.POSITIVE_INFINITY
94134
} else {
95-
try {
96-
this.rightLabelContainer?.remove()
97-
} catch (err) {
98-
logger.error(`Error in VTSourceRendererBase.mountRightLabelContainer 1: ${stringifyError(err)}`)
99-
}
100-
itemElement.appendChild(this.rightLabelContainer)
135+
this.removeAuxiliaryNode(this.rightLabelContainer)
101136
newState.rightLabelIsAppendage = false
102137
}
103138
} else if (prevProps?.partDuration !== props.partDuration) {
104-
if (itemDuration === Number.POSITIVE_INFINITY && this.state.rightLabelIsAppendage !== true) {
105-
itemElement.parentElement?.parentElement?.parentElement?.appendChild(this.rightLabelContainer)
106-
107-
newState.rightLabelIsAppendage = true
108-
} else if (itemDuration !== Number.POSITIVE_INFINITY && this.state.rightLabelIsAppendage === true) {
109-
try {
110-
this.rightLabelContainer?.remove()
111-
} catch (err) {
112-
logger.error(`Error in VTSourceRendererBase.mountRightLabelContainer 2: ${stringifyError(err)}`)
113-
}
114-
itemElement.appendChild(this.rightLabelContainer)
139+
if (targetElement) {
140+
this.mountAuxiliaryNode(this.rightLabelContainer, targetElement)
141+
newState.rightLabelIsAppendage = itemDuration === Number.POSITIVE_INFINITY
142+
} else if (this.state.rightLabelIsAppendage !== false) {
143+
this.removeAuxiliaryNode(this.rightLabelContainer)
115144
newState.rightLabelIsAppendage = false
116145
}
117146
}
@@ -126,32 +155,25 @@ class VTSourceRendererBase extends CustomLayerItemRenderer<IProps & WithTranslat
126155
itemElement: HTMLElement | null
127156
): Partial<IState> {
128157
const { relative: relativeRendering, isLiveLine, outputLayer } = props
158+
const targetElement = this.getCountdownTarget(itemElement)
129159
if (
130160
this.countdownContainer &&
131161
!this.state.sourceEndCountdownAppendage &&
132162
!relativeRendering &&
133163
isLiveLine &&
134164
!outputLayer.collapsed &&
135-
itemElement
165+
targetElement
136166
) {
137-
const liveLine =
138-
itemElement.parentElement?.parentElement?.parentElement?.parentElement?.parentElement?.querySelector(
139-
'.segment-timeline__liveline'
140-
)
141-
if (liveLine) {
142-
liveLine.appendChild(this.countdownContainer)
167+
if (targetElement) {
168+
this.mountAuxiliaryNode(this.countdownContainer, targetElement)
143169
newState.sourceEndCountdownAppendage = true
144170
}
145171
} else if (
146172
this.countdownContainer &&
147173
this.state.sourceEndCountdownAppendage &&
148-
!(!relativeRendering && isLiveLine && !outputLayer.collapsed && itemElement)
174+
!(!relativeRendering && isLiveLine && !outputLayer.collapsed && targetElement)
149175
) {
150-
try {
151-
this.countdownContainer.remove()
152-
} catch (err) {
153-
logger.error(`Error in VTSourceRendererBase.mountSourceEndedCountdownContainer 1: ${stringifyError(err)}`)
154-
}
176+
this.removeAuxiliaryNode(this.countdownContainer)
155177
newState.sourceEndCountdownAppendage = false
156178
}
157179

@@ -224,23 +246,10 @@ class VTSourceRendererBase extends CustomLayerItemRenderer<IProps & WithTranslat
224246
super.componentWillUnmount()
225247
}
226248

227-
if (this.rightLabelContainer) {
228-
try {
229-
this.rightLabelContainer.remove()
230-
} catch (err) {
231-
logger.error(`Error in VTSourceRendererBase.componentWillUnmount 1: ${stringifyError(err)}`)
232-
}
233-
this.rightLabelContainer = null
234-
}
235-
236-
if (this.countdownContainer) {
237-
try {
238-
this.countdownContainer.remove()
239-
} catch (err) {
240-
logger.error(`Error in VTSourceRendererBase.componentWillUnmount 2: ${stringifyError(err)}`)
241-
}
242-
this.countdownContainer = null
243-
}
249+
this.removeAuxiliaryNode(this.rightLabelContainer)
250+
this.removeAuxiliaryNode(this.countdownContainer)
251+
this.rightLabelContainer = null
252+
this.countdownContainer = null
244253
}
245254

246255
private renderLeftLabel() {

0 commit comments

Comments
 (0)