From d34280c03dcb3a521c387b72fb552013c7a5637d Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Sun, 26 Jan 2025 13:14:24 +0100 Subject: [PATCH] More batch (#1468) * batch updates in client * rearange updating logic * utilety type * guard undefinde in affectsLoaded * fix: pause state not saved to restore point --- apps/client/src/common/stores/runtime.ts | 12 ++ apps/client/src/common/utils/socket.ts | 28 +++-- .../runtime-service/RuntimeService.ts | 112 +++++++++++------- .../runtime-service/rundownService.utils.ts | 7 +- apps/server/src/stores/EventStore.ts | 1 + apps/server/src/stores/runtimeState.ts | 6 +- 6 files changed, 108 insertions(+), 58 deletions(-) diff --git a/apps/client/src/common/stores/runtime.ts b/apps/client/src/common/stores/runtime.ts index e8a2919324..70ac4c9934 100644 --- a/apps/client/src/common/stores/runtime.ts +++ b/apps/client/src/common/stores/runtime.ts @@ -14,6 +14,18 @@ export const runtimeStore = createWithEqualityFn( export const useRuntimeStore = (selector: (state: RuntimeStore) => T) => useStoreWithEqualityFn(runtimeStore, selector, deepCompare); +let batchStore: Partial = {}; + +export function addToBatchUpdates(key: K, value: RuntimeStore[K]) { + batchStore[key] = value; +} + +export function flushBatchUpdates() { + const state = runtimeStore.getState(); + runtimeStore.setState({ ...state, ...batchStore }); + batchStore = {}; +} + /** * Allows patching a property of the runtime store */ diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index 4af7af8167..f61360122e 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -14,7 +14,7 @@ import { } from '../stores/clientStore'; import { addDialog } from '../stores/dialogStore'; import { addLog } from '../stores/logger'; -import { patchRuntime, patchRuntimeProperty } from '../stores/runtime'; +import { addToBatchUpdates, flushBatchUpdates, patchRuntime, patchRuntimeProperty } from '../stores/runtime'; export let websocket: WebSocket | null = null; let reconnectTimeout: NodeJS.Timeout | null = null; @@ -140,57 +140,57 @@ export const connectSocket = () => { break; } case 'ontime-clock': { - patchRuntimeProperty('clock', payload); + addToBatchUpdates('clock', payload); updateDevTools({ clock: payload }); break; } case 'ontime-timer': { - patchRuntimeProperty('timer', payload); + addToBatchUpdates('timer', payload); updateDevTools({ timer: payload }); break; } case 'ontime-onAir': { - patchRuntimeProperty('onAir', payload); + addToBatchUpdates('onAir', payload); updateDevTools({ onAir: payload }); break; } case 'ontime-message': { - patchRuntimeProperty('message', payload); + addToBatchUpdates('message', payload); updateDevTools({ message: payload }); break; } case 'ontime-runtime': { - patchRuntimeProperty('runtime', payload); + addToBatchUpdates('runtime', payload); updateDevTools({ runtime: payload }); break; } case 'ontime-eventNow': { - patchRuntimeProperty('eventNow', payload); + addToBatchUpdates('eventNow', payload); updateDevTools({ eventNow: payload }); break; } case 'ontime-currentBlock': { - patchRuntimeProperty('currentBlock', payload); + addToBatchUpdates('currentBlock', payload); updateDevTools({ currentBlock: payload }); break; } case 'ontime-publicEventNow': { - patchRuntimeProperty('publicEventNow', payload); + addToBatchUpdates('publicEventNow', payload); updateDevTools({ publicEventNow: payload }); break; } case 'ontime-eventNext': { - patchRuntimeProperty('eventNext', payload); + addToBatchUpdates('eventNext', payload); updateDevTools({ eventNext: payload }); break; } case 'ontime-publicEventNext': { - patchRuntimeProperty('publicEventNext', payload); + addToBatchUpdates('publicEventNext', payload); updateDevTools({ publicEventNext: payload }); break; } case 'ontime-auxtimer1': { - patchRuntimeProperty('auxtimer1', payload); + addToBatchUpdates('auxtimer1', payload); updateDevTools({ auxtimer1: payload }); break; } @@ -207,6 +207,10 @@ export const connectSocket = () => { } break; } + case 'ontime-flush': { + flushBatchUpdates() + break; + } } } catch (_) { // ignore unhandled diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index a43ef29803..2d6fe844ff 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -6,7 +6,6 @@ import { MaybeNumber, OntimeEvent, Playback, - RuntimeStore, TimerLifeCycle, TimerPhase, TimerState, @@ -22,7 +21,7 @@ import { timerConfig } from '../../config/config.js'; import { eventStore } from '../../stores/EventStore.js'; import { EventTimer } from '../EventTimer.js'; -import { type RestorePoint, restoreService } from '../RestoreService.js'; +import { RestorePoint, restoreService } from '../RestoreService.js'; import { findNext, findPrevious, @@ -37,6 +36,8 @@ import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './ru import { skippedOutOfEvent } from '../timerUtils.js'; import { triggerAutomations } from '../../api-data/automation/automation.service.js'; +type RuntimeStateEventKeys = keyof Pick; + /** * Service manages runtime status of app * Coordinating with necessary services @@ -181,10 +182,10 @@ class RuntimeService { const next = state.eventNext?.id; const nextPublic = state.publicEventNext?.id; return ( - affectedIds.includes(now) || - affectedIds.includes(nowPublic) || - affectedIds.includes(next) || - affectedIds.includes(nextPublic) + (now !== undefined && affectedIds.includes(now)) || + (nowPublic !== undefined && affectedIds.includes(nowPublic)) || + (next !== undefined && affectedIds.includes(next)) || + (nextPublic !== undefined && affectedIds.includes(nextPublic)) ); } @@ -681,71 +682,100 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert // we do the comparison by explicitly for each property // to apply custom logic for different datasets - const shouldForceTimerUpdate = getForceUpdate( - RuntimeService.previousTimerUpdate, - state.clock, - state.timer.playback, - ); - - const shouldUpdateTimer = - shouldForceTimerUpdate || getShouldTimerUpdate(RuntimeService.previousTimerValue, state.timer.current); - - const shouldRuntimeUpdate = shouldUpdateTimer || getForceUpdate(RuntimeService.previousRuntimeUpdate, state.clock); - - // some changes need an immediate update + // if a new event was loaded most things should update const hasNewLoaded = state.eventNow?.id !== RuntimeService.previousState?.eventNow?.id; + // for the very fist run there will be nothing in the previousState so we force an update const justStarted = !RuntimeService.previousState?.timer; + + // if playback changes most things should update const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback; + + // combine all big changes const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback; + /** + * Timer should be updated if + * - big changes + * - notification rate has been exceeded + * - the timer has rolled over into the next UI display unit + * + * Then check if there is actually a change in the data + */ + const shouldUpdateTimer = + (hasImmediateChanges || + getForceUpdate(RuntimeService.previousTimerUpdate, state.clock) || + getShouldTimerUpdate(RuntimeService.previousTimerValue, state.timer.current)) && + !deepEqual(RuntimeService.previousState?.timer, state.timer); + + /** + * Runtime should be updated if + * - big changes + * - the timer is updating so runtime also updates to keep them in sync ??? + * - notification rate has been exceeded + * + * Then check if there is actually a change in the data + */ + const shouldRuntimeUpdate = + (hasImmediateChanges || shouldUpdateTimer || getForceUpdate(RuntimeService.previousRuntimeUpdate, state.clock)) && + !deepEqual(RuntimeService.previousState?.runtime, state.runtime); + + /** + * the currentBlock object has no ticking values so we only need to check for equality + */ + const shouldBlockUpdate = !deepEqual(RuntimeService?.previousState.currentBlock, state.currentBlock); + + /** + * Many other values are calculated based on the clock + * so if any of them are updated we also need to send the clock + * in case nothing else is updating the clock will bw updated at the notification rate + */ + const shouldUpdateClock = + shouldUpdateTimer || + shouldRuntimeUpdate || + shouldBlockUpdate || + getForceUpdate(RuntimeService.previousClockUpdate, state.clock); + + //Now we set all the updates on the eventstore and update the previous value if (hasChangedPlayback) { eventStore.set('onAir', state.timer.playback !== Playback.Stop); } - if (hasImmediateChanges || (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.timer, state.timer))) { + if (shouldUpdateTimer) { + eventStore.set('timer', state.timer); RuntimeService.previousTimerUpdate = state.clock; RuntimeService.previousTimerValue = state.timer.current; - RuntimeService.previousClockUpdate = state.clock; - eventStore.set('clock', state.clock); - eventStore.set('timer', state.timer); RuntimeService.previousState.timer = { ...state.timer }; } - if ( - hasChangedPlayback || - (shouldRuntimeUpdate && !deepEqual(RuntimeService.previousState?.runtime, state.runtime)) - ) { + if (shouldRuntimeUpdate) { eventStore.set('runtime', state.runtime); - RuntimeService.previousClockUpdate = state.clock; RuntimeService.previousRuntimeUpdate = state.clock; - eventStore.set('clock', state.clock); RuntimeService.previousState.runtime = { ...state.runtime }; } - // Update the events if they have changed - updateEventIfChanged('eventNow', state); - updateEventIfChanged('publicEventNow', state); - updateEventIfChanged('eventNext', state); - updateEventIfChanged('publicEventNext', state); - - if (!deepEqual(RuntimeService?.previousState.currentBlock, state.currentBlock)) { + if (shouldBlockUpdate) { eventStore.set('currentBlock', state.currentBlock); RuntimeService.previousState.currentBlock = { ...state.currentBlock }; - RuntimeService.previousClockUpdate = state.clock; - eventStore.set('clock', state.clock); } - const shouldUpdateClock = getShouldClockUpdate(RuntimeService.previousClockUpdate, state.clock); + if (hasImmediateChanges) { + saveRestoreState(state); + } if (shouldUpdateClock) { RuntimeService.previousClockUpdate = state.clock; eventStore.set('clock', state.clock); - saveRestoreState(state); } + // Update the events if they have changed + updateEventIfChanged('eventNow', state); + updateEventIfChanged('publicEventNow', state); + updateEventIfChanged('eventNext', state); + updateEventIfChanged('publicEventNext', state); + // Helper function to update an event if it has changed - function updateEventIfChanged(eventKey: keyof RuntimeStore, state: runtimeState.RuntimeState) { + function updateEventIfChanged(eventKey: RuntimeStateEventKeys, state: runtimeState.RuntimeState) { const previous = RuntimeService.previousState?.[eventKey]; const now = state[eventKey]; @@ -766,7 +796,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert return; } - function storeKey(eventKey: keyof RuntimeStore) { + function storeKey(eventKey: RuntimeStateEventKeys) { eventStore.set(eventKey, state[eventKey]); RuntimeService.previousState[eventKey] = { ...state[eventKey] }; } diff --git a/apps/server/src/services/runtime-service/rundownService.utils.ts b/apps/server/src/services/runtime-service/rundownService.utils.ts index ca0b851aca..ece819e7ca 100644 --- a/apps/server/src/services/runtime-service/rundownService.utils.ts +++ b/apps/server/src/services/runtime-service/rundownService.utils.ts @@ -1,7 +1,7 @@ import { millisToSeconds } from 'ontime-utils'; import { timerConfig } from '../../config/config.js'; -import { MaybeNumber, Playback } from 'ontime-types'; +import { MaybeNumber } from 'ontime-types'; /** * Checks whether we should update the clock value @@ -21,7 +21,7 @@ export function getShouldClockUpdate(previousUpdate: number, now: number): boole * Checks whether we should update the timer value * - we have rolled into a new seconds unit */ -export function getShouldTimerUpdate(previousValue: number, currentValue: MaybeNumber): boolean { +export function getShouldTimerUpdate(previousValue: MaybeNumber, currentValue: MaybeNumber): boolean { if (currentValue === null) { return false; } @@ -36,8 +36,7 @@ export function getShouldTimerUpdate(previousValue: number, currentValue: MaybeN * - if we have escaped the update rate (clock slid forward) * - if we are not playing then there is no need to update the timer */ -export function getForceUpdate(previousUpdate: number, now: number, playbackState: Playback = Playback.Play): boolean { - if (playbackState !== Playback.Play) return false; +export function getForceUpdate(previousUpdate: number, now: number): boolean { const isClockBehind = now < previousUpdate; const hasExceededRate = now - previousUpdate >= timerConfig.notificationRate; return isClockBehind || hasExceededRate; diff --git a/apps/server/src/stores/EventStore.ts b/apps/server/src/stores/EventStore.ts index 20c55e0bae..85390d85ae 100644 --- a/apps/server/src/stores/EventStore.ts +++ b/apps/server/src/stores/EventStore.ts @@ -35,6 +35,7 @@ export const eventStore = { for (const dataKey of changedKeys) { socket.sendAsJson({ type: `ontime-${dataKey}`, payload: store[dataKey] }); } + socket.sendAsJson({ type: `ontime-flush` }); isUpdatePending = null; changedKeys.clear(); }); diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index 0df5c805e2..7f7ceb502f 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -136,10 +136,14 @@ export function clear() { * Utility to allow modifying the state from the outside * @param newState */ -function patchTimer(newState: Partial) { +function patchTimer(newState: Partial) { for (const key in newState) { if (key in runtimeState.timer) { runtimeState.timer[key] = newState[key]; + } else if (key in runtimeState._timer) { + // in case of a RestorePoint we will receive a pausedAt value + // wiche is needed to resume a paused timer + runtimeState._timer[key] = newState[key]; } } }