Skip to content

Commit 4740206

Browse files
committed
feat(webui): Reduce render churn
1 parent c63d1db commit 4740206

4 files changed

Lines changed: 268 additions & 107 deletions

File tree

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

Lines changed: 106 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { type PropsWithChildren } from 'react'
22
import { Meteor } from 'meteor/meteor'
33
import { withTracker } from '../../../lib/ReactMeteorData/react-meteor-data.js'
4+
import _ from 'underscore'
45
import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString'
56
import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist'
67
import { RundownTiming, type TimeEventArgs } from './RundownTiming.js'
@@ -59,6 +60,31 @@ interface IRundownTimingProviderTrackedProps {
5960
partsInQuickLoop: Record<TimingId, boolean>
6061
}
6162

63+
function getPlaylistRenderSelector(playlist: DBRundownPlaylist | undefined) {
64+
if (!playlist) return undefined
65+
66+
const quickLoopStartId =
67+
playlist.quickLoop?.start && 'id' in playlist.quickLoop.start ? playlist.quickLoop.start.id : undefined
68+
const quickLoopEndId =
69+
playlist.quickLoop?.end && 'id' in playlist.quickLoop.end ? playlist.quickLoop.end.id : undefined
70+
71+
return {
72+
id: playlist._id,
73+
activationId: playlist.activationId,
74+
rundownIdsInOrder: playlist.rundownIdsInOrder,
75+
currentPartInstanceId: playlist.currentPartInfo?.partInstanceId,
76+
nextPartInstanceId: playlist.nextPartInfo?.partInstanceId,
77+
previousPartInstanceId: playlist.previousPartInfo?.partInstanceId,
78+
outOfOrderTiming: playlist.outOfOrderTiming,
79+
segmentsStartedPlayback: playlist.segmentsStartedPlayback,
80+
quickLoopRunning: playlist.quickLoop?.running,
81+
quickLoopStartType: playlist.quickLoop?.start?.type,
82+
quickLoopStartId,
83+
quickLoopEndType: playlist.quickLoop?.end?.type,
84+
quickLoopEndId,
85+
}
86+
}
87+
6288
/**
6389
* RundownTimingProvider is a container component that provides a timing context to all child elements.
6490
* It allows calculating a single
@@ -69,93 +95,101 @@ export const RundownTimingProvider = withTracker<
6995
PropsWithChildren<IRundownTimingProviderProps>,
7096
IRundownTimingProviderState,
7197
IRundownTimingProviderTrackedProps
72-
>(({ playlist }) => {
73-
if (!playlist) {
74-
return {
75-
rundowns: [],
76-
currentRundown: undefined,
77-
partInstances: [],
78-
partInstancesMap: new Map(),
79-
segments: [],
80-
segmentsMap: new Map(),
81-
partsInQuickLoop: {},
98+
>(
99+
({ playlist }) => {
100+
if (!playlist) {
101+
return {
102+
rundowns: [],
103+
currentRundown: undefined,
104+
partInstances: [],
105+
partInstancesMap: new Map(),
106+
segments: [],
107+
segmentsMap: new Map(),
108+
partsInQuickLoop: {},
109+
}
82110
}
83-
}
84111

85-
const partInstancesMap = new Map<PartId, MinimalPartInstance>()
86-
87-
const rundowns = RundownPlaylistCollectionUtil.getRundownsOrdered(playlist)
88-
const segments = RundownPlaylistClientUtil.getSegments(playlist)
89-
const segmentsMap = new Map<SegmentId, DBSegment>(segments.map((segment) => [segment._id, segment]))
90-
const unorderedParts = RundownPlaylistClientUtil.getUnorderedParts(playlist)
91-
const activePartInstances = RundownPlaylistClientUtil.getActivePartInstances(playlist, undefined, {
92-
projection: {
93-
_id: 1,
94-
rundownId: 1,
95-
segmentId: 1,
96-
isTemporary: 1,
97-
segmentPlayoutId: 1,
98-
takeCount: 1,
99-
part: 1,
100-
timings: 1,
101-
orphaned: 1,
102-
},
103-
}) as Array<
104-
Pick<
105-
PartInstance,
106-
| '_id'
107-
| 'rundownId'
108-
| 'segmentId'
109-
| 'isTemporary'
110-
| 'segmentPlayoutId'
111-
| 'takeCount'
112-
| 'part'
113-
| 'timings'
114-
| 'orphaned'
112+
const partInstancesMap = new Map<PartId, MinimalPartInstance>()
113+
114+
const rundowns = RundownPlaylistCollectionUtil.getRundownsOrdered(playlist)
115+
const segments = RundownPlaylistClientUtil.getSegments(playlist)
116+
const segmentsMap = new Map<SegmentId, DBSegment>(segments.map((segment) => [segment._id, segment]))
117+
const unorderedParts = RundownPlaylistClientUtil.getUnorderedParts(playlist)
118+
const activePartInstances = RundownPlaylistClientUtil.getActivePartInstances(playlist, undefined, {
119+
projection: {
120+
_id: 1,
121+
rundownId: 1,
122+
segmentId: 1,
123+
isTemporary: 1,
124+
segmentPlayoutId: 1,
125+
takeCount: 1,
126+
part: 1,
127+
timings: 1,
128+
orphaned: 1,
129+
},
130+
}) as Array<
131+
Pick<
132+
PartInstance,
133+
| '_id'
134+
| 'rundownId'
135+
| 'segmentId'
136+
| 'isTemporary'
137+
| 'segmentPlayoutId'
138+
| 'takeCount'
139+
| 'part'
140+
| 'timings'
141+
| 'orphaned'
142+
>
115143
>
116-
>
117144

118-
const { currentPartInstance } = findCurrentAndPreviousPartInstance(
119-
activePartInstances,
120-
playlist.currentPartInfo?.partInstanceId,
121-
playlist.previousPartInfo?.partInstanceId
122-
)
145+
const { currentPartInstance } = findCurrentAndPreviousPartInstance(
146+
activePartInstances,
147+
playlist.currentPartInfo?.partInstanceId,
148+
playlist.previousPartInfo?.partInstanceId
149+
)
123150

124-
const currentRundown = currentPartInstance
125-
? rundowns.find((r) => r._id === currentPartInstance.rundownId)
126-
: rundowns[0]
151+
const currentRundown = currentPartInstance
152+
? rundowns.find((r) => r._id === currentPartInstance.rundownId)
153+
: rundowns[0]
127154

128-
let partInstances: MinimalPartInstance[] = []
155+
let partInstances: MinimalPartInstance[] = []
129156

130-
const allPartIds: Set<PartId> = new Set()
157+
const allPartIds: Set<PartId> = new Set()
131158

132-
for (const partInstance of activePartInstances) {
133-
allPartIds.add(partInstance.part._id)
134-
}
159+
for (const partInstance of activePartInstances) {
160+
allPartIds.add(partInstance.part._id)
161+
}
135162

136-
partInstances = activePartInstances
163+
partInstances = activePartInstances
137164

138-
for (const part of unorderedParts) {
139-
if (allPartIds.has(part._id)) continue
140-
partInstances.push(wrapPartToTemporaryInstance(playlist.activationId ?? protectString(''), part))
141-
}
165+
for (const part of unorderedParts) {
166+
if (allPartIds.has(part._id)) continue
167+
partInstances.push(wrapPartToTemporaryInstance(playlist.activationId ?? protectString(''), part))
168+
}
142169

143-
partInstances = sortPartInstancesInSortedSegments(partInstances, segments)
170+
partInstances = sortPartInstancesInSortedSegments(partInstances, segments)
144171

145-
partInstances = deduplicatePartInstancesForQuickLoop(playlist, partInstances, currentPartInstance)
172+
partInstances = deduplicatePartInstancesForQuickLoop(playlist, partInstances, currentPartInstance)
146173

147-
const partsInQuickLoop = findPartInstancesInQuickLoop(playlist, partInstances)
174+
const partsInQuickLoop = findPartInstancesInQuickLoop(playlist, partInstances)
148175

149-
return {
150-
rundowns,
151-
currentRundown,
152-
partInstances,
153-
partInstancesMap,
154-
segments,
155-
segmentsMap,
156-
partsInQuickLoop,
176+
return {
177+
rundowns,
178+
currentRundown,
179+
partInstances,
180+
partInstancesMap,
181+
segments,
182+
segmentsMap,
183+
partsInQuickLoop,
184+
}
185+
},
186+
(_data, props, nextProps) => {
187+
if (props.refreshInterval !== nextProps.refreshInterval) return true
188+
if (props.defaultDuration !== nextProps.defaultDuration) return true
189+
190+
return !_.isEqual(getPlaylistRenderSelector(props.playlist), getPlaylistRenderSelector(nextProps.playlist))
157191
}
158-
})(
192+
)(
159193
class RundownTimingProvider extends React.Component<
160194
PropsWithChildren<IRundownTimingProviderProps> & IRundownTimingProviderTrackedProps,
161195
IRundownTimingProviderState

packages/webui/src/client/ui/SegmentContainer/withResolvedSegment.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -139,23 +139,30 @@ export function withResolvedSegment<T extends IResolvedSegmentProps, IState = {}
139139

140140
// This registers a reactive dependency on infinites-capping pieces, so that the segment can be
141141
// re-evaluated when a piece like that appears.
142-
PieceInstances.find({
143-
rundownId: segment.rundownId,
144-
dynamicallyInserted: {
145-
$exists: true,
146-
},
147-
'infinite.fromPreviousPart': false,
148-
'piece.lifespan': {
149-
$in: [
150-
PieceLifespan.OutOnRundownEnd,
151-
PieceLifespan.OutOnRundownChange,
152-
PieceLifespan.OutOnShowStyleEnd,
153-
],
154-
},
155-
reset: {
156-
$ne: true,
142+
PieceInstances.find(
143+
{
144+
rundownId: segment.rundownId,
145+
dynamicallyInserted: {
146+
$exists: true,
147+
},
148+
'infinite.fromPreviousPart': false,
149+
'piece.lifespan': {
150+
$in: [
151+
PieceLifespan.OutOnRundownEnd,
152+
PieceLifespan.OutOnRundownChange,
153+
PieceLifespan.OutOnShowStyleEnd,
154+
],
155+
},
156+
reset: {
157+
$ne: true,
158+
},
157159
},
158-
}).fetch()
160+
{
161+
fields: {
162+
_id: 1,
163+
},
164+
}
165+
).count()
159166

160167
const [orderedAllPartIds, { currentPartInstance, nextPartInstance }] = slowDownReactivity(
161168
() =>
@@ -345,6 +352,7 @@ export function withResolvedSegment<T extends IResolvedSegmentProps, IState = {}
345352
)
346353
)
347354
}
355+
const segmentContainsNextOrCurrentPart = !!(data.parts && findNextOrCurrentPart(data.parts))
348356
// Check rundown changes that are important to the segment
349357
if (
350358
typeof props.playlist !== typeof nextProps.playlist ||
@@ -354,10 +362,10 @@ export function withResolvedSegment<T extends IResolvedSegmentProps, IState = {}
354362
((props.playlist.currentPartInfo?.partInstanceId !==
355363
nextProps.playlist.currentPartInfo?.partInstanceId ||
356364
props.playlist.nextPartInfo?.partInstanceId !== nextProps.playlist.nextPartInfo?.partInstanceId) &&
357-
data.parts &&
358-
findNextOrCurrentPart(data.parts)) ||
359-
props.playlist.holdState !== nextProps.playlist.holdState ||
360-
props.playlist.nextTimeOffset !== nextProps.playlist.nextTimeOffset ||
365+
segmentContainsNextOrCurrentPart) ||
366+
((props.playlist.holdState !== nextProps.playlist.holdState ||
367+
props.playlist.nextTimeOffset !== nextProps.playlist.nextTimeOffset) &&
368+
segmentContainsNextOrCurrentPart) ||
361369
props.playlist.activationId !== nextProps.playlist.activationId ||
362370
!_.isEqual(props.playlist.quickLoop?.start, nextProps.playlist.quickLoop?.start) ||
363371
!_.isEqual(props.playlist.quickLoop?.end, nextProps.playlist.quickLoop?.end) ||

packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,10 @@ export class SegmentTimelinePartClass extends React.Component<Translated<WithTim
357357
const firstPartTimingId = props.firstPartInSegment
358358
? getPartInstanceTimingId(props.firstPartInSegment.instance)
359359
: undefined
360-
const lastPartTimingId = props.lastPartInSegment
361-
? getPartInstanceTimingId(props.lastPartInSegment.instance)
362-
: undefined
360+
const lastPartTimingId =
361+
props.isBudgetGap && props.lastPartInSegment
362+
? getPartInstanceTimingId(props.lastPartInSegment.instance)
363+
: undefined
363364

364365
return {
365366
partDuration: props.timingDurations.partDurations?.[timingId],
@@ -369,9 +370,6 @@ export class SegmentTimelinePartClass extends React.Component<Translated<WithTim
369370
firstPartDisplayStart: firstPartTimingId
370371
? props.timingDurations.partDisplayStartsAt?.[firstPartTimingId]
371372
: undefined,
372-
firstPartDisplayDuration: firstPartTimingId
373-
? props.timingDurations.partDisplayDurations?.[firstPartTimingId]
374-
: undefined,
375373
lastPartDisplayStart: lastPartTimingId
376374
? props.timingDurations.partDisplayStartsAt?.[lastPartTimingId]
377375
: undefined,
@@ -452,8 +450,7 @@ export class SegmentTimelinePartClass extends React.Component<Translated<WithTim
452450
nextProps: Readonly<Translated<WithTiming<IProps>>>
453451
): boolean {
454452
return (
455-
prevProps.studio._id !== nextProps.studio._id ||
456-
!_.isEqual(prevProps.studio.settings, nextProps.studio.settings)
453+
prevProps.studio._id !== nextProps.studio._id || !_.isEqual(prevProps.studio.settings, nextProps.studio.settings)
457454
)
458455
}
459456

@@ -1138,13 +1135,18 @@ export const SegmentTimelinePart: React.ComponentType<IProps> = withTranslation(
11381135
const firstPartInSegmentId = props.firstPartInSegment
11391136
? getPartInstanceTimingId(props.firstPartInSegment.instance)
11401137
: undefined
1138+
const lastPartInSegmentId =
1139+
props.isBudgetGap && props.lastPartInSegment
1140+
? getPartInstanceTimingId(props.lastPartInSegment.instance)
1141+
: undefined
11411142
return [
11421143
(durations.partDurations || {})[timingId],
11431144
(durations.partDisplayStartsAt || {})[timingId],
11441145
(durations.partDisplayDurations || {})[timingId],
11451146
(durations.partsInQuickLoop || {})[timingId],
11461147
firstPartInSegmentId ? (durations.partDisplayStartsAt || {})[firstPartInSegmentId] : undefined,
1147-
firstPartInSegmentId ? (durations.partDisplayDurations || {})[firstPartInSegmentId] : undefined,
1148+
lastPartInSegmentId ? (durations.partDisplayStartsAt || {})[lastPartInSegmentId] : undefined,
1149+
lastPartInSegmentId ? (durations.partDisplayDurations || {})[lastPartInSegmentId] : undefined,
11481150
]
11491151
},
11501152
}

0 commit comments

Comments
 (0)