Skip to content

Commit

Permalink
More batch (#1468)
Browse files Browse the repository at this point in the history
* batch updates in client

* rearange updating logic

* utilety type

* guard undefinde in affectsLoaded

* fix: pause state not saved to restore point
  • Loading branch information
alex-Arc authored Jan 26, 2025
1 parent bb1e907 commit d34280c
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 58 deletions.
12 changes: 12 additions & 0 deletions apps/client/src/common/stores/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ export const runtimeStore = createWithEqualityFn<RuntimeStore>(
export const useRuntimeStore = <T>(selector: (state: RuntimeStore) => T) =>
useStoreWithEqualityFn(runtimeStore, selector, deepCompare);

let batchStore: Partial<RuntimeStore> = {};

export function addToBatchUpdates<K extends keyof RuntimeStore>(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
*/
Expand Down
28 changes: 16 additions & 12 deletions apps/client/src/common/utils/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -207,6 +207,10 @@ export const connectSocket = () => {
}
break;
}
case 'ontime-flush': {
flushBatchUpdates()
break;
}
}
} catch (_) {
// ignore unhandled
Expand Down
112 changes: 71 additions & 41 deletions apps/server/src/services/runtime-service/RuntimeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
MaybeNumber,
OntimeEvent,
Playback,
RuntimeStore,
TimerLifeCycle,
TimerPhase,
TimerState,
Expand All @@ -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,
Expand All @@ -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<RuntimeState, 'eventNext' | 'eventNow' | 'publicEventNow' | 'publicEventNext'>;

/**
* Service manages runtime status of app
* Coordinating with necessary services
Expand Down Expand Up @@ -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))
);
}

Expand Down Expand Up @@ -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];

Expand All @@ -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] };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
}
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/stores/EventStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/stores/runtimeState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,14 @@ export function clear() {
* Utility to allow modifying the state from the outside
* @param newState
*/
function patchTimer(newState: Partial<TimerState>) {
function patchTimer(newState: Partial<TimerState & RestorePoint>) {
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];
}
}
}
Expand Down

0 comments on commit d34280c

Please sign in to comment.