Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More batch #1468

Merged
merged 5 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
alex-Arc marked this conversation as resolved.
Show resolved Hide resolved

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))
alex-Arc marked this conversation as resolved.
Show resolved Hide resolved
);
}

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
Loading