From eff9f1d15a7ad80a76618cbc44c54b7f11f732c0 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Wed, 8 Jan 2025 15:38:48 +0100 Subject: [PATCH 1/6] specific rounding for UI updating --- .../runtime-service/rundownService.utils.ts | 6 +-- packages/utils/index.ts | 1 + .../src/date-utils/conversionUtils.test.ts | 50 ++++++++++++++++++- .../utils/src/date-utils/conversionUtils.ts | 27 ++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/apps/server/src/services/runtime-service/rundownService.utils.ts b/apps/server/src/services/runtime-service/rundownService.utils.ts index ca0b851aca..9b43e5bf21 100644 --- a/apps/server/src/services/runtime-service/rundownService.utils.ts +++ b/apps/server/src/services/runtime-service/rundownService.utils.ts @@ -1,4 +1,4 @@ -import { millisToSeconds } from 'ontime-utils'; +import { millisToUISeconds } from 'ontime-utils'; import { timerConfig } from '../../config/config.js'; import { MaybeNumber, Playback } from 'ontime-types'; @@ -13,7 +13,7 @@ export function getShouldClockUpdate(previousUpdate: number, now: number): boole if (shouldForceUpdate) { return true; } - const isClockSecondAhead = millisToSeconds(now) !== millisToSeconds(previousUpdate + timerConfig.triggerAhead); + const isClockSecondAhead = millisToUISeconds(now) !== millisToUISeconds(previousUpdate + timerConfig.triggerAhead); return isClockSecondAhead; } @@ -26,7 +26,7 @@ export function getShouldTimerUpdate(previousValue: number, currentValue: MaybeN return false; } // we avoid trigger ahead since it can cause duplicate triggers - const shouldUpdateTimer = millisToSeconds(currentValue) !== millisToSeconds(previousValue); + const shouldUpdateTimer = millisToUISeconds(currentValue) !== millisToUISeconds(previousValue); return shouldUpdateTimer; } diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 23681c87fc..e37ebe250f 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -42,6 +42,7 @@ export { millisToHours, millisToMinutes, millisToSeconds, + millisToUISeconds, secondsInMillis, } from './src/date-utils/conversionUtils.js'; export { isISO8601, isTimeString } from './src/date-utils/isTimeString.js'; diff --git a/packages/utils/src/date-utils/conversionUtils.test.ts b/packages/utils/src/date-utils/conversionUtils.test.ts index 5cf99c11d6..3ad4526b22 100644 --- a/packages/utils/src/date-utils/conversionUtils.test.ts +++ b/packages/utils/src/date-utils/conversionUtils.test.ts @@ -1,4 +1,52 @@ -import { millisToHours, millisToMinutes, millisToSeconds, secondsInMillis } from './conversionUtils'; +import { millisToHours, millisToMinutes, millisToSeconds, millisToUISeconds, secondsInMillis } from './conversionUtils'; + +describe('millisToUITimer()', () => { + test('null values', () => { + const t = { val: null, result: 0 }; + expect(millisToUISeconds(t.val)).toBe(t.result); + }); + test('1 sec', () => { + const t = { val: 1000, result: 1 }; + expect(millisToUISeconds(t.val)).toBe(t.result); + }); + test('alsmot 1 sec', () => { + const t = { val: 1032, result: 1 }; + expect(millisToUISeconds(t.val)).toBe(t.result); + }); + test('just past 1 sec', () => { + const t = { val: 1000 - 32, result: 1 }; + expect(millisToUISeconds(t.val)).toBe(t.result); + }); + test('half way 1 sec', () => { + const t = { val: 500, result: 1 }; + expect(millisToUISeconds(t.val)).toBe(t.result); + }); + + test('zero', () => { + const t = { val: 32, result: 0 }; + expect(millisToUISeconds(t.val)).toBe(t.result); + }); + + test('zero', () => { + const t = { val: 0, result: 0 }; + expect(millisToUISeconds(t.val)).toBe(t.result); + }); + + test('negative', () => { + const t = { val: -32, result: 0 }; + expect(millisToUISeconds(t.val)).toBe(t.result); + }); + + test('negative', () => { + const t = { val: -1000 + 33, result: 0 }; + expect(millisToUISeconds(t.val)).toBe(t.result); + }); + + test('negative', () => { + const t = { val: -1000 + 31, result: -1 }; + expect(millisToUISeconds(t.val)).toBe(t.result); + }); +}); describe('millisToSecond()', () => { test('null values', () => { diff --git a/packages/utils/src/date-utils/conversionUtils.ts b/packages/utils/src/date-utils/conversionUtils.ts index d245fe1b54..ed1a41f855 100644 --- a/packages/utils/src/date-utils/conversionUtils.ts +++ b/packages/utils/src/date-utils/conversionUtils.ts @@ -29,6 +29,33 @@ export function millisToSeconds(millis: MaybeNumber): number { return convertMillis(millis, MILLIS_PER_SECOND); } +export function millisToUISeconds(millis: MaybeNumber): number { + if (millis === null) { + return 0; + } + + const isNegative = millis < 0; + const val = Math.abs(millis); + + const remainder = val % MILLIS_PER_SECOND; + + if (isNegative) { + if (remainder <= 1000 - 32) { + const ret = Math.ceil(millis / MILLIS_PER_SECOND); + // eslint-disable-next-line no-compare-neg-zero + return ret === -0 ? 0 : ret; + } + + return Math.floor(millis / MILLIS_PER_SECOND); + } + + if (remainder <= 32) { + return Math.floor(val / MILLIS_PER_SECOND); + } + + return Math.ceil(val / MILLIS_PER_SECOND); +} + /** * Converts value in milliseconds to minutes * @param millis From 783bf7a5faae30f4f0c0816c7ca1820b6353b010 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Fri, 24 Jan 2025 16:48:15 +0100 Subject: [PATCH 2/6] only update clock after 1sec where not ther updates ocurred --- apps/server/src/services/runtime-service/RuntimeService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index c9e79dcdb7..4c8d47cfc3 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -736,7 +736,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert eventStore.set('clock', state.clock); } - const shouldUpdateClock = getShouldClockUpdate(RuntimeService.previousClockUpdate, state.clock); + const shouldUpdateClock = getForceUpdate(RuntimeService.previousClockUpdate, state.clock); if (shouldUpdateClock) { RuntimeService.previousClockUpdate = state.clock; From a05f46ef3f45609e3c62072f222a857bf5b7811d Mon Sep 17 00:00:00 2001 From: arc-alex Date: Fri, 24 Jan 2025 16:50:22 +0100 Subject: [PATCH 3/6] rearange update logic rearange --- .../runtime-service/RuntimeService.ts | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index 4c8d47cfc3..188b8fd0a8 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -681,17 +681,6 @@ 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 const hasNewLoaded = state.eventNow?.id !== RuntimeService.previousState?.eventNow?.id; @@ -699,11 +688,19 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback; const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback; + const shouldUpdateTimer = + hasImmediateChanges || + getForceUpdate(RuntimeService.previousTimerUpdate, state.clock, state.timer.playback) || + getShouldTimerUpdate(RuntimeService.previousTimerValue, state.timer.current); + + const shouldRuntimeUpdate = + hasImmediateChanges || getShouldTimerUpdate(RuntimeService.previousState.runtime.offset, state.runtime.offset); + if (hasChangedPlayback) { eventStore.set('onAir', state.timer.playback !== Playback.Stop); } - if (hasImmediateChanges || (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.timer, state.timer))) { + if (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.timer, state.timer)) { RuntimeService.previousTimerUpdate = state.clock; RuntimeService.previousTimerValue = state.timer.current; RuntimeService.previousClockUpdate = state.clock; @@ -712,10 +709,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert RuntimeService.previousState.timer = { ...state.timer }; } - if ( - hasChangedPlayback || - (shouldRuntimeUpdate && !deepEqual(RuntimeService.previousState?.runtime, state.runtime)) - ) { + if (shouldRuntimeUpdate && !deepEqual(RuntimeService.previousState?.runtime, state.runtime)) { eventStore.set('runtime', state.runtime); RuntimeService.previousClockUpdate = state.clock; RuntimeService.previousRuntimeUpdate = state.clock; @@ -723,12 +717,6 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert 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)) { eventStore.set('currentBlock', state.currentBlock); RuntimeService.previousState.currentBlock = { ...state.currentBlock }; @@ -736,14 +724,23 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert eventStore.set('clock', state.clock); } + if (hasImmediateChanges) { + saveRestoreState(state); + } + const shouldUpdateClock = getForceUpdate(RuntimeService.previousClockUpdate, state.clock); 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) { const previous = RuntimeService.previousState?.[eventKey]; From af366adb82d69c0b8b602d569871a7f478e700b7 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Fri, 24 Jan 2025 16:54:56 +0100 Subject: [PATCH 4/6] getShouldTimerUpdate takse MaybeNumber --- .../server/src/services/runtime-service/rundownService.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/services/runtime-service/rundownService.utils.ts b/apps/server/src/services/runtime-service/rundownService.utils.ts index 9b43e5bf21..bf9d78de17 100644 --- a/apps/server/src/services/runtime-service/rundownService.utils.ts +++ b/apps/server/src/services/runtime-service/rundownService.utils.ts @@ -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; } From 4a8734c8001f7432527a5c1922b59bcd41eac007 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Fri, 24 Jan 2025 17:28:53 +0100 Subject: [PATCH 5/6] fixup! rearange update logic --- .../runtime-service/RuntimeService.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index 188b8fd0a8..7ef34182ae 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -689,31 +689,28 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback; const shouldUpdateTimer = - hasImmediateChanges || - getForceUpdate(RuntimeService.previousTimerUpdate, state.clock, state.timer.playback) || - getShouldTimerUpdate(RuntimeService.previousTimerValue, state.timer.current); + (getForceUpdate(RuntimeService.previousTimerUpdate, state.clock, state.timer.playback) || + getShouldTimerUpdate(RuntimeService.previousTimerValue, state.timer.current)) && + !deepEqual(RuntimeService.previousState?.timer, state.timer); const shouldRuntimeUpdate = - hasImmediateChanges || getShouldTimerUpdate(RuntimeService.previousState.runtime.offset, state.runtime.offset); + (shouldUpdateTimer || getForceUpdate(RuntimeService.previousRuntimeUpdate, state.clock)) && + !deepEqual(RuntimeService.previousState?.runtime, state.runtime); if (hasChangedPlayback) { eventStore.set('onAir', state.timer.playback !== Playback.Stop); } - if (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.timer, state.timer)) { + if (hasImmediateChanges || shouldUpdateTimer) { 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 (shouldRuntimeUpdate && !deepEqual(RuntimeService.previousState?.runtime, state.runtime)) { + if (hasImmediateChanges || shouldRuntimeUpdate) { eventStore.set('runtime', state.runtime); - RuntimeService.previousClockUpdate = state.clock; RuntimeService.previousRuntimeUpdate = state.clock; - eventStore.set('clock', state.clock); RuntimeService.previousState.runtime = { ...state.runtime }; } @@ -728,7 +725,8 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert saveRestoreState(state); } - const shouldUpdateClock = getForceUpdate(RuntimeService.previousClockUpdate, state.clock); + const shouldUpdateClock = + shouldUpdateTimer || shouldRuntimeUpdate || getForceUpdate(RuntimeService.previousClockUpdate, state.clock); if (shouldUpdateClock) { RuntimeService.previousClockUpdate = state.clock; From dd170a6bacbdf78c9f96f4649346e1f45da223cf Mon Sep 17 00:00:00 2001 From: arc-alex Date: Fri, 24 Jan 2025 17:32:33 +0100 Subject: [PATCH 6/6] batch updates in client --- apps/client/src/common/stores/runtime.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/client/src/common/stores/runtime.ts b/apps/client/src/common/stores/runtime.ts index e8a2919324..1e78e98905 100644 --- a/apps/client/src/common/stores/runtime.ts +++ b/apps/client/src/common/stores/runtime.ts @@ -14,13 +14,22 @@ export const runtimeStore = createWithEqualityFn( export const useRuntimeStore = (selector: (state: RuntimeStore) => T) => useStoreWithEqualityFn(runtimeStore, selector, deepCompare); +let batchStore: Partial = {}; +let isUpdatePending: NodeJS.Timeout | null = null; + /** * Allows patching a property of the runtime store */ export function patchRuntimeProperty(key: K, value: RuntimeStore[K]) { - const state = runtimeStore.getState(); - state[key] = value; - runtimeStore.setState({ ...state }); + batchStore[key] = value; + if (!isUpdatePending) { + isUpdatePending = setTimeout(() => { + const state = runtimeStore.getState(); + runtimeStore.setState({ ...state, ...batchStore }); + batchStore = {}; + isUpdatePending = null; + }); + } } /**