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

Batch client updates #1448

Merged
merged 5 commits into from
Jan 24, 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
28 changes: 22 additions & 6 deletions apps/server/src/services/runtime-service/RuntimeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class RuntimeService {

/** last time we updated the socket */
static previousTimerUpdate: number;
static previousRuntimeUpdate: number;
static previousTimerValue: MaybeNumber; // previous timer value, could be null
static previousClockUpdate: number;

Expand All @@ -58,6 +59,7 @@ class RuntimeService {
this.eventTimer = eventTimer;

RuntimeService.previousTimerUpdate = -1;
RuntimeService.previousRuntimeUpdate = -1;
RuntimeService.previousTimerValue = -1;
RuntimeService.previousClockUpdate = -1;
RuntimeService.previousState = {} as RuntimeState;
Expand Down Expand Up @@ -678,10 +680,17 @@ 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);
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
const hasNewLoaded = state.eventNow?.id !== RuntimeService.previousState?.eventNow?.id;

Expand All @@ -696,12 +705,20 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
if (hasImmediateChanges || (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.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 || (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.runtime, state.runtime))) {
if (
hasChangedPlayback ||
(shouldRuntimeUpdate && !deepEqual(RuntimeService.previousState?.runtime, state.runtime))
) {
eventStore.set('runtime', state.runtime);
RuntimeService.previousClockUpdate = state.clock;
RuntimeService.previousRuntimeUpdate = state.clock;
eventStore.set('clock', state.clock);
RuntimeService.previousState.runtime = { ...state.runtime };
}

Expand All @@ -711,15 +728,14 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
updateEventIfChanged('eventNext', state);
updateEventIfChanged('publicEventNext', state);

let syncBlockStartAt = false;

if (!deepEqual(RuntimeService?.previousState.currentBlock, state.currentBlock)) {
eventStore.set('currentBlock', state.currentBlock);
RuntimeService.previousState.currentBlock = { ...state.currentBlock };
syncBlockStartAt = true;
RuntimeService.previousClockUpdate = state.clock;
eventStore.set('clock', state.clock);
}

const shouldUpdateClock = syncBlockStartAt || getShouldClockUpdate(RuntimeService.previousClockUpdate, state.clock);
const shouldUpdateClock = getShouldClockUpdate(RuntimeService.previousClockUpdate, state.clock);

if (shouldUpdateClock) {
RuntimeService.previousClockUpdate = state.clock;
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 } from 'ontime-types';
import { MaybeNumber, Playback } from 'ontime-types';

/**
* Checks whether we should update the clock value
Expand Down Expand Up @@ -34,8 +34,10 @@ export function getShouldTimerUpdate(previousValue: number, currentValue: MaybeN
* In some cases we want to force an update to the timer
* - if the clock has slid back
* - 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): boolean {
export function getForceUpdate(previousUpdate: number, now: number, playbackState: Playback = Playback.Play): boolean {
if (playbackState !== Playback.Play) return false;
const isClockBehind = now < previousUpdate;
const hasExceededRate = now - previousUpdate >= timerConfig.notificationRate;
return isClockBehind || hasExceededRate;
Expand Down
26 changes: 16 additions & 10 deletions apps/server/src/stores/EventStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type StoreGetter = <T extends keyof RuntimeStore>(key: T) => Partial<Runt

let store: Partial<RuntimeStore> = {};

const changedKeys = new Set<keyof RuntimeStore>();
let isUpdatePending: NodeJS.Immediate | null = null;
cpvalente marked this conversation as resolved.
Show resolved Hide resolved
/**
* A runtime store that broadcasts its payload
* - init: allows for adding an initial payload to the store
Expand All @@ -23,16 +25,20 @@ export const eventStore = {
},
set<T extends keyof RuntimeStore>(key: T, value: RuntimeStore[T]) {
store[key] = value;
socket.sendAsJson({
type: `ontime-${key}`,
payload: value,
});
},
batchSet(values: Partial<RuntimeStore>) {
Object.entries(values).forEach(([key, value]) => {
store[key] = value;
});
this.broadcast();

// check if the key is already marked for and update otherwise push it onto the update array
changedKeys.add(key);

//if there is already and update pending we don't need to schedule another one
if (!isUpdatePending) {
isUpdatePending = setImmediate(() => {
for (const dataKey of changedKeys) {
socket.sendAsJson({ type: `ontime-${dataKey}`, payload: store[dataKey] });
}
isUpdatePending = null;
changedKeys.clear();
});
}
},
poll() {
return store;
Expand Down
Loading