Skip to content

Commit af39869

Browse files
committed
Merge branch 'fix-memory-leaks' into bbc-main-autonext
2 parents a0586cd + dd4d50e commit af39869

9 files changed

Lines changed: 121 additions & 30 deletions

File tree

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

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ export class ElementObserverManager {
413413
private resizeObserver: ResizeObserver
414414
private mutationObserver: MutationObserver
415415
private observedElements: Map<HTMLElement, () => void>
416+
private pendingReconnectFrame: number | undefined
416417

417418
private constructor() {
418419
this.observedElements = new Map()
@@ -467,40 +468,58 @@ export class ElementObserverManager {
467468

468469
this.observedElements.set(element, callback)
469470
this.resizeObserver.observe(element)
470-
this.mutationObserver.observe(element, {
471-
childList: true,
472-
subtree: true,
473-
attributes: true,
474-
characterData: true,
475-
})
471+
if (!this.pendingReconnectFrame) {
472+
this.mutationObserver.observe(element, {
473+
childList: true,
474+
subtree: true,
475+
attributes: true,
476+
characterData: true,
477+
})
478+
}
476479
}
477480

478481
public unobserve(element: HTMLElement): void {
479482
if (!element) return
480483
this.observedElements.delete(element)
481484
this.resizeObserver.unobserve(element)
482485

483-
// Disconnect and reconnect mutation observer to refresh the list of observed elements
484-
this.mutationObserver.disconnect()
485-
for (const observedElement of this.observedElements.keys()) {
486-
if (!document.contains(observedElement)) {
487-
this.observedElements.delete(observedElement)
488-
this.resizeObserver.unobserve(observedElement)
489-
}
490-
}
491-
492486
if (this.observedElements.size === 0) {
487+
if (this.pendingReconnectFrame) {
488+
window.cancelAnimationFrame(this.pendingReconnectFrame)
489+
this.pendingReconnectFrame = undefined
490+
}
491+
this.mutationObserver.disconnect()
493492
this.resizeObserver.disconnect()
494493
return
495494
}
496495

497-
this.observedElements.forEach((_, el) => {
498-
this.mutationObserver.observe(el, {
499-
childList: true,
500-
subtree: true,
501-
attributes: true,
502-
characterData: true,
496+
if (!this.pendingReconnectFrame) {
497+
this.pendingReconnectFrame = window.requestAnimationFrame(() => {
498+
this.pendingReconnectFrame = undefined
499+
500+
// MutationObserver has no per-element unobserve, so we reconnect once per frame.
501+
this.mutationObserver.disconnect()
502+
for (const observedElement of this.observedElements.keys()) {
503+
if (!document.contains(observedElement)) {
504+
this.observedElements.delete(observedElement)
505+
this.resizeObserver.unobserve(observedElement)
506+
}
507+
}
508+
509+
if (this.observedElements.size === 0) {
510+
this.resizeObserver.disconnect()
511+
return
512+
}
513+
514+
this.observedElements.forEach((_, el) => {
515+
this.mutationObserver.observe(el, {
516+
childList: true,
517+
subtree: true,
518+
attributes: true,
519+
characterData: true,
520+
})
521+
})
503522
})
504-
})
523+
}
505524
}
506525
}

packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,18 @@ export function CameraScreen({ playlist, studioId }: Readonly<IProps>): JSX.Elem
147147
useSetDocumentDarkTheme()
148148

149149
useEffect(() => {
150-
const getContainerEl = () => document.querySelector('#render-target > .container-fluid.header-clear')
150+
const getContainerEl = () => document.querySelector('#render-target > .container-fluid')
151151
const containerEl = getContainerEl()
152-
if (containerEl) containerEl.classList.remove('header-clear')
152+
if (containerEl instanceof HTMLElement) containerEl.classList.remove('header-clear')
153153

154154
return () => {
155+
if (containerEl instanceof HTMLElement && containerEl.isConnected) {
156+
containerEl.classList.add('header-clear')
157+
return
158+
}
159+
155160
const currentContainerEl = getContainerEl()
156-
if (currentContainerEl && currentContainerEl === containerEl) {
161+
if (currentContainerEl instanceof HTMLElement) {
157162
currentContainerEl.classList.add('header-clear')
158163
}
159164
}

packages/webui/src/client/ui/SegmentList/LinePartMainPiece/LinePartMainPiece.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type EvsContent, SourceLayerType } from '@sofie-automation/blueprints-integration'
2-
import React, { useContext, useMemo, useRef, useState } from 'react'
2+
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
33
// TODO: Move to a shared lib file
44
import type { PartId, PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids'
55
import classNames from 'classnames'
@@ -162,6 +162,15 @@ export function LinePartMainPiece({
162162
}
163163
}
164164

165+
useEffect(() => {
166+
return () => {
167+
if (previewSession.current) {
168+
previewSession.current.close()
169+
previewSession.current = null
170+
}
171+
}
172+
}, [])
173+
165174
const onPointerLeave = (e: React.PointerEvent<HTMLDivElement>) => {
166175
if (e.pointerType !== 'mouse') {
167176
return

packages/webui/src/client/ui/SegmentList/LinePartPieceIndicator/LinePartScriptPiece.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SourceLayerType } from '@sofie-automation/blueprints-integration'
2-
import { useContext, useMemo, useRef } from 'react'
2+
import { useContext, useEffect, useMemo, useRef } from 'react'
33
import {
44
PreviewPopUpContext,
55
type IPreviewPopUpSession,
@@ -44,6 +44,15 @@ export function LinePartScriptPiece({ pieces }: IProps): JSX.Element {
4444
})
4545
}
4646

47+
useEffect(() => {
48+
return () => {
49+
if (previewSession.current) {
50+
previewSession.current.close()
51+
previewSession.current = null
52+
}
53+
}
54+
}, [])
55+
4756
function onMouseLeave() {
4857
if (previewSession.current) {
4958
previewSession.current.close()

packages/webui/src/client/ui/SegmentList/LinePartSecondaryPiece/LinePartSecondaryPiece.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import classNames from 'classnames'
2-
import React, { type CSSProperties, useCallback, useContext, useMemo, useRef } from 'react'
2+
import React, { type CSSProperties, useCallback, useContext, useEffect, useMemo, useRef } from 'react'
33
import { useContentStatusForPieceInstance } from '../../SegmentTimeline/withMediaObjectStatus.js'
44
import {
55
PreviewPopUpContext,
@@ -64,6 +64,15 @@ export const LinePartSecondaryPiece: React.FC<IProps> = React.memo(function Line
6464
})
6565
}
6666

67+
useEffect(() => {
68+
return () => {
69+
if (previewSession.current) {
70+
previewSession.current.close()
71+
previewSession.current = null
72+
}
73+
}
74+
}, [])
75+
6776
const onPointerLeave = (e: React.PointerEvent<HTMLDivElement>) => {
6877
if (e.pointerType !== 'mouse') {
6978
return

packages/webui/src/client/ui/SegmentStoryboard/StoryboardPartSecondaryPieces/StoryboardSecondaryPiece.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useImperativeHandle, useContext, useRef, useState, type RefObject } from 'react'
1+
import { useImperativeHandle, useContext, useEffect, useRef, useState, type RefObject } from 'react'
22
import { type ISourceLayer, SourceLayerType } from '@sofie-automation/blueprints-integration'
33
import { DefaultRenderer } from './Renderers/DefaultRenderer.js'
44
import { assertNever } from '@sofie-automation/corelib/dist/lib'
@@ -129,6 +129,15 @@ export function StoryboardSecondaryPiece(props: IProps): JSX.Element {
129129
if (onPointerEnterCallback) onPointerEnterCallback(e)
130130
}
131131

132+
useEffect(() => {
133+
return () => {
134+
if (previewSession.current) {
135+
previewSession.current.close()
136+
previewSession.current = null
137+
}
138+
}
139+
}, [])
140+
132141
const onPointerLeave = (e: React.PointerEvent<HTMLDivElement>) => {
133142
setHovering(null)
134143

packages/webui/src/client/ui/SegmentStoryboard/StoryboardPartThumbnail/StoryboardPartThumbnailInner.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useContext, useRef, useState } from 'react'
1+
import { useContext, useEffect, useRef, useState } from 'react'
22
import type { ISourceLayer } from '@sofie-automation/blueprints-integration'
33
import { getElementDocumentOffset, type OffsetPosition } from '../../../utils/positions.js'
44
import { getElementHeight, getElementWidth } from '../../../utils/dimensions.js'
@@ -83,6 +83,15 @@ export function StoryboardPartThumbnailInner({
8383
})
8484
}
8585

86+
useEffect(() => {
87+
return () => {
88+
if (previewSession.current) {
89+
previewSession.current.close()
90+
previewSession.current = null
91+
}
92+
}
93+
}, [])
94+
8695
const onPointerLeave = (e: React.PointerEvent<HTMLDivElement>) => {
8796
if (e.pointerType !== 'mouse') {
8897
return

packages/webui/src/client/ui/Shelf/Renderers/L3rdListItemRenderer.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ export const L3rdListItemRenderer: React.FunctionComponent<ILayerItemRendererPro
130130
}
131131
}
132132

133+
useEffect(() => {
134+
return () => {
135+
if (previewSession.current) {
136+
previewSession.current.close()
137+
previewSession.current = null
138+
}
139+
}
140+
}, [])
141+
133142
const type = props.adLibListItem.isAction
134143
? props.adLibListItem.isGlobal
135144
? 'rundownBaselineAdLibAction'

packages/webui/src/client/ui/Shelf/Renderers/VTListItemRenderer.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,25 @@ export const VTListItemRenderer: React.FunctionComponent<ILayerItemRendererProps
8989
}
9090
}
9191

92+
useEffect(() => {
93+
return () => {
94+
if (previewSession.current) {
95+
previewSession.current.close()
96+
previewSession.current = null
97+
}
98+
}
99+
}, [])
100+
92101
const handleOnMouseMove = (e: React.MouseEvent) => {
93102
if (itemIconPosition) {
94103
const left = e.pageX - itemIconPosition.left
95104
let unprocessedPercentage = left / itemIconPosition.width
96105
if ((unprocessedPercentage > 1 || unprocessedPercentage < 0) && showMiniInspector) {
97106
setShowMiniInspector(false)
107+
if (previewSession.current) {
108+
previewSession.current.close()
109+
previewSession.current = null
110+
}
98111
return false
99112
} else if (unprocessedPercentage >= 0 && unprocessedPercentage <= 1 && !showMiniInspector) {
100113
setShowMiniInspector(true)

0 commit comments

Comments
 (0)