From c57b0423af2d11b55067ab24b759966568909be1 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sat, 11 May 2024 11:02:40 +0200 Subject: [PATCH] refactor: leading updates in timers --- apps/server/src/config/config.ts | 2 +- apps/server/src/services/TimerService.ts | 3 +- .../runtime-service/RuntimeService.ts | 35 +++++++---- .../__tests__/rundownService.utils.test.ts | 61 +++++++++++++++++++ .../runtime-service/rundownService.utils.ts | 54 ++++++++++++++++ 5 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 apps/server/src/services/runtime-service/__tests__/rundownService.utils.test.ts create mode 100644 apps/server/src/services/runtime-service/rundownService.utils.ts diff --git a/apps/server/src/config/config.ts b/apps/server/src/config/config.ts index f6d85900c..e2d0b540c 100644 --- a/apps/server/src/config/config.ts +++ b/apps/server/src/config/config.ts @@ -2,5 +2,5 @@ export const timerConfig = { skipLimit: 1000, // threshold of skip for recalculating updateRate: 32, // how often do we update the timer notificationRate: 1000, // how often do we notify clients and integrations - triggerAhead: 16, // how far ahead do we trigger the end event + triggerAhead: 10, // how far ahead do we trigger the end event }; diff --git a/apps/server/src/services/TimerService.ts b/apps/server/src/services/TimerService.ts index 03adb868e..d4852581b 100644 --- a/apps/server/src/services/TimerService.ts +++ b/apps/server/src/services/TimerService.ts @@ -2,6 +2,7 @@ import { OntimeEvent } from 'ontime-types'; import * as runtimeState from '../stores/runtimeState.js'; import type { UpdateResult } from '../stores/runtimeState.js'; +import { timerConfig } from '../config/config.js'; /** * Service manages Ontime's main timer @@ -41,7 +42,7 @@ export class TimerService { } const state = runtimeState.getState(); - const endTime = state.timer.current - 10; + const endTime = state.timer.current - timerConfig.triggerAhead; this.endCallback = setTimeout(() => this.update(), endTime); return true; } diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index 6b007b446..f764afc40 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -1,4 +1,4 @@ -import { EndAction, LogOrigin, OntimeEvent, Playback, RuntimeStore, TimerLifeCycle } from 'ontime-types'; +import { EndAction, LogOrigin, MaybeNumber, OntimeEvent, Playback, RuntimeStore, TimerLifeCycle } from 'ontime-types'; import { millisToString, validatePlayback } from 'ontime-utils'; import { deepEqual } from 'fast-equals'; @@ -20,6 +20,7 @@ import { getPlayableEvents, } from '../rundown-service/rundownUtils.js'; import { integrationService } from '../integration-service/IntegrationService.js'; +import { getShouldClockUpdate, getShouldTimerUpdate } from './rundownService.utils.js'; /** * Service manages runtime status of app @@ -30,12 +31,16 @@ class RuntimeService { private lastOnUpdate = -1; /** last time we updated the socket */ - static previousUpdate: number; + static previousTimerUpdate: number; + static previousTimerValue: MaybeNumber; + static previousClockUpdate: number; /** last known state */ static previousState: RuntimeState; constructor() { - RuntimeService.previousUpdate = -1; + RuntimeService.previousTimerUpdate = -1; + RuntimeService.previousTimerValue = -1; + RuntimeService.previousClockUpdate = -1; RuntimeService.previousState = {} as RuntimeState; } @@ -516,29 +521,33 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert // we do the comparison by explicitly for each property // to apply custom logic for different datasets - // some of the data, we only update at intervals - const isTimeToUpdate = - state.clock < RuntimeService.previousUpdate || - state.clock - RuntimeService.previousUpdate >= timerConfig.notificationRate; + const shouldUpdateClock = getShouldClockUpdate(RuntimeService.previousClockUpdate, state.clock); + const shouldUpdateTimer = getShouldTimerUpdate( + RuntimeService.previousTimerValue, + state.timer.current, + RuntimeService.previousTimerUpdate, + state.clock, + ); // some changes need an immediate update const hasNewLoaded = state.eventNow?.id !== RuntimeService.previousState?.eventNow?.id; - const hasSkippedBack = state.clock < RuntimeService.previousUpdate; const justStarted = !RuntimeService.previousState?.timer; const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback; - const hasImmediateChanges = hasNewLoaded || hasSkippedBack || justStarted || hasChangedPlayback; + const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback; if (hasChangedPlayback) { eventStore.set('onAir', state.timer.playback !== Playback.Stop); } - if (hasImmediateChanges || (isTimeToUpdate && !deepEqual(RuntimeService.previousState?.timer, state.timer))) { + if (hasImmediateChanges || (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.timer, state.timer))) { + RuntimeService.previousTimerUpdate = state.clock; + RuntimeService.previousTimerValue = state.timer.current; eventStore.set('timer', state.timer); RuntimeService.previousState.timer = { ...state.timer }; } - if (hasChangedPlayback || (isTimeToUpdate && !deepEqual(RuntimeService.previousState?.runtime, state.runtime))) { + if (hasChangedPlayback || (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.runtime, state.runtime))) { eventStore.set('runtime', state.runtime); RuntimeService.previousState.runtime = { ...state.runtime }; } @@ -549,8 +558,8 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert updateEventIfChanged('eventNext', state); updateEventIfChanged('publicEventNext', state); - if (isTimeToUpdate) { - RuntimeService.previousUpdate = state.clock; + if (shouldUpdateClock) { + RuntimeService.previousClockUpdate = state.clock; eventStore.set('clock', state.clock); saveRestoreState(state); } diff --git a/apps/server/src/services/runtime-service/__tests__/rundownService.utils.test.ts b/apps/server/src/services/runtime-service/__tests__/rundownService.utils.test.ts new file mode 100644 index 000000000..41910f8d1 --- /dev/null +++ b/apps/server/src/services/runtime-service/__tests__/rundownService.utils.test.ts @@ -0,0 +1,61 @@ +import { MILLIS_PER_MINUTE } from 'ontime-utils'; +import { getShouldClockUpdate, getShouldTimerUpdate } from '../rundownService.utils.js'; + +describe('getShouldClockUpdate()', () => { + it('should return true when we slid forwards', () => { + const previousUpdate = Date.now() - 2000; // 2 seconds ago + const now = Date.now(); + const result = getShouldClockUpdate(previousUpdate, now); + expect(result).toBe(true); + }); + + it('should return true when we slid backwards', () => { + const previousUpdate = Date.now(); + const now = Date.now() - 2000; // 2 seconds ago + const result = getShouldClockUpdate(previousUpdate, now); + expect(result).toBe(true); + }); + + it('should return true when clock is a second ahead', () => { + const previousUpdate = MILLIS_PER_MINUTE - 100; + const now = MILLIS_PER_MINUTE; + const result = getShouldClockUpdate(previousUpdate, now); + expect(result).toBe(true); + }); + + it('should return false when clock is not a second ahead and force update is not required', () => { + const previousUpdate = Date.now() - 32; + const now = Date.now(); + const result = getShouldClockUpdate(previousUpdate, now); + expect(result).toBe(false); + }); +}); + +describe('getShouldTimerUpdate', () => { + it('should return false when currentValue is null', () => { + const previousValue = 0; + const currentValue = null; + const previousUpdate = 0; + const now = 0; + const result = getShouldTimerUpdate(previousValue, currentValue, previousUpdate, now); + expect(result).toBe(false); + }); + + it('should return true when timer is a second ahead', () => { + const previousValue = 5000; + const currentValue = 6500; + const previousUpdate = Date.now(); + const now = Date.now(); + const result = getShouldTimerUpdate(previousValue, currentValue, previousUpdate, now); + expect(result).toBe(true); + }); + + it('should return false when timer is not a second ahead and force update is not required', () => { + const previousValue = 5000; + const currentValue = 5500; + const previousUpdate = Date.now(); + const now = Date.now(); + const result = getShouldTimerUpdate(previousValue, currentValue, previousUpdate, now); + expect(result).toBe(false); + }); +}); diff --git a/apps/server/src/services/runtime-service/rundownService.utils.ts b/apps/server/src/services/runtime-service/rundownService.utils.ts new file mode 100644 index 000000000..b8760f9a9 --- /dev/null +++ b/apps/server/src/services/runtime-service/rundownService.utils.ts @@ -0,0 +1,54 @@ +import { millisToSeconds } from 'ontime-utils'; + +import { timerConfig } from '../../config/config.js'; +import { MaybeNumber } from 'ontime-types'; + +/** + * Checks whether we should update the clock value + * - clock has slid + * - we have rolled into a new seconds unit + */ +export function getShouldClockUpdate(previousUpdate: number, now: number): boolean { + const shouldForceUpdate = getForceUpdate(previousUpdate, now); + if (shouldForceUpdate) { + return true; + } + const isClockSecondAhead = millisToSeconds(now) !== millisToSeconds(previousUpdate + timerConfig.triggerAhead); + return isClockSecondAhead; +} + +/** + * Checks whether we should update the timer value + * - timer has slid (?) + * - we have rolled into a new seconds unit + */ +export function getShouldTimerUpdate( + previousValue: number, + currentValue: MaybeNumber, + previousUpdate: number, + now: number, +): boolean { + if (currentValue === null) { + return false; + } + + const shouldForceUpdate = getForceUpdate(previousUpdate, now); + if (shouldForceUpdate) { + return true; + } + + const shouldUpdateTimer = millisToSeconds(currentValue) !== millisToSeconds(previousValue + timerConfig.triggerAhead); + + return shouldUpdateTimer; +} + +/** + * 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) + */ +function getForceUpdate(previousUpdate: number, now: number): boolean { + const isClockBehind = now < previousUpdate; + const hasExceededRate = now - previousUpdate >= timerConfig.notificationRate; + return isClockBehind || hasExceededRate; +}