refactor: improve performance in rundown

- leverage compiler
- correct subscriptions from zustand
This commit is contained in:
Carlos Valente
2025-12-31 10:39:26 +01:00
committed by Carlos Valente
parent 0de55e74a8
commit 8e32314667
20 changed files with 61 additions and 63 deletions
@@ -33,7 +33,7 @@ export default function useRundown() {
export function useRundownWithMetadata() { export function useRundownWithMetadata() {
const { data, status } = useRundown(); const { data, status } = useRundown();
const { selectedEventId } = useSelectedEventId(); const selectedEventId = useSelectedEventId();
const rundownMetadata = useMemo(() => getRundownMetadata(data, selectedEventId), [data, selectedEventId]); const rundownMetadata = useMemo(() => getRundownMetadata(data, selectedEventId), [data, selectedEventId]);
return { data, status, rundownMetadata }; return { data, status, rundownMetadata };
} }
@@ -57,7 +57,7 @@ export function useFlatRundown() {
export function useFlatRundownWithMetadata() { export function useFlatRundownWithMetadata() {
const { data, status } = useRundown(); const { data, status } = useRundown();
const { selectedEventId } = useSelectedEventId(); const selectedEventId = useSelectedEventId();
const rundownWithMetadata = useMemo(() => getFlatRundownMetadata(data, selectedEventId), [data, selectedEventId]); const rundownWithMetadata = useMemo(() => getFlatRundownMetadata(data, selectedEventId), [data, selectedEventId]);
return { data: rundownWithMetadata, status }; return { data: rundownWithMetadata, status };
+8 -28
View File
@@ -128,13 +128,9 @@ export const setAuxTimer = {
setDuration: (index: number, time: number) => sendSocket('auxtimer', { [index]: { duration: time } }), setDuration: (index: number, time: number) => sendSocket('auxtimer', { [index]: { duration: time } }),
}; };
export const useSelectedEventId = createSelector((state: RuntimeStore) => ({ export const useSelectedEventId = createSelector((state: RuntimeStore) => state.eventNow?.id ?? null);
selectedEventId: state.eventNow?.id ?? null,
}));
export const useCurrentGroupId = createSelector((state: RuntimeStore) => ({ export const useCurrentGroupId = createSelector((state: RuntimeStore) => state.groupNow?.id ?? null);
currentGroupId: state.groupNow?.id ?? null,
}));
export const setEventPlayback = { export const setEventPlayback = {
loadEvent: (id: string) => sendSocket('load', { id }), loadEvent: (id: string) => sendSocket('load', { id }),
@@ -147,9 +143,7 @@ export const useTimer = createSelector((state: RuntimeStore) => ({
...state.timer, ...state.timer,
})); }));
export const useClock = createSelector((state: RuntimeStore) => ({ export const useClock = createSelector((state: RuntimeStore) => state.clock);
clock: state.clock,
}));
export const useNextFlag = createSelector((state: RuntimeStore) => ({ export const useNextFlag = createSelector((state: RuntimeStore) => ({
id: state.eventFlag?.id ?? null, id: state.eventFlag?.id ?? null,
@@ -173,28 +167,16 @@ export const useExpectedStartData = createSelector((state: RuntimeStore) => ({
clock: state.clock, clock: state.clock,
})); }));
export const usePing = createSelector((state: RuntimeStore) => ({ export const usePing = createSelector((state: RuntimeStore) => state.ping);
ping: state.ping,
}));
/** convert ping into a derived value which changes less often */ /** convert ping into a derived value which changes less often */
export const useIsOnline = createSelector((state: RuntimeStore) => ({ export const useIsOnline = createSelector((state: RuntimeStore) => state.ping > 0);
isOnline: state.ping > 0,
}));
export const useOffsetMode = createSelector((state: RuntimeStore) => ({ export const useOffsetMode = createSelector((state: RuntimeStore) => state.offset.mode);
offsetMode: state.offset.mode,
}));
export const setOffsetMode = (payload: OffsetMode) => sendSocket('offsetmode', payload); export const setOffsetMode = (payload: OffsetMode) => sendSocket('offsetmode', payload);
export const usePlayback = () => { export const usePlayback = createSelector((state: RuntimeStore) => state.timer.playback);
const featureSelector = (state: RuntimeStore) => ({
playback: state.timer.playback,
});
return useRuntimeStore(featureSelector);
};
/* ======================= Overview data subscriptions ======================= */ /* ======================= Overview data subscriptions ======================= */
@@ -204,9 +186,7 @@ export const useStartTimesOverview = createSelector((state: RuntimeStore) => ({
plannedEnd: state.rundown.plannedEnd, plannedEnd: state.rundown.plannedEnd,
})); }));
export const useRundownExpectedEnd = createSelector((state: RuntimeStore) => ({ export const useRundownExpectedEnd = createSelector((state: RuntimeStore) => state.offset.expectedRundownEnd);
expectedEnd: state.offset.expectedRundownEnd,
}));
export const useProgressOverview = createSelector((state: RuntimeStore) => ({ export const useProgressOverview = createSelector((state: RuntimeStore) => ({
numEvents: state.rundown.numEvents, numEvents: state.rundown.numEvents,
@@ -34,7 +34,7 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) {
} }
function OntimeCloudStats() { function OntimeCloudStats() {
const { ping } = usePing(); const ping = usePing();
/** /**
* Send immediate ping request, and keep sending on an interval * Send immediate ping request, and keep sending on an interval
@@ -43,7 +43,7 @@ export default function OperatorLoader() {
} }
function Operator({ rundown, rundownMetadata, customFields, settings }: OperatorData) { function Operator({ rundown, rundownMetadata, customFields, settings }: OperatorData) {
const { selectedEventId } = useSelectedEventId(); const selectedEventId = useSelectedEventId();
const { subscribe, mainSource, secondarySource, shouldEdit, hidePast, showStart } = useOperatorOptions(); const { subscribe, mainSource, secondarySource, shouldEdit, hidePast, showStart } = useOperatorOptions();
const [showEditPrompt, setShowEditPrompt] = useState(false); const [showEditPrompt, setShowEditPrompt] = useState(false);
@@ -11,7 +11,8 @@ interface OverviewWrapperProps {
} }
export function OverviewWrapper({ navElements, children }: PropsWithChildren<OverviewWrapperProps>) { export function OverviewWrapper({ navElements, children }: PropsWithChildren<OverviewWrapperProps>) {
const { isOnline } = useIsOnline(); const isOnline = useIsOnline();
return ( return (
<div className={cx([style.overview, !isOnline && style.isOffline])}> <div className={cx([style.overview, !isOnline && style.isOffline])}>
<ErrorBoundary> <ErrorBoundary>
@@ -127,7 +127,7 @@ export function StartTimes({ shouldFormat }: OverviewTimeElementsProps) {
* Extracted to improve performance as this is a ticking value * Extracted to improve performance as this is a ticking value
*/ */
function RundownExpectedEnd({ shouldFormat }: OverviewTimeElementsProps) { function RundownExpectedEnd({ shouldFormat }: OverviewTimeElementsProps) {
const { expectedEnd } = useRundownExpectedEnd(); const expectedEnd = useRundownExpectedEnd();
const [maybeExpectedEnd, maybeExpectedDaySpan] = useMemo(() => calculateEndAndDaySpan(expectedEnd), [expectedEnd]); const [maybeExpectedEnd, maybeExpectedDaySpan] = useMemo(() => calculateEndAndDaySpan(expectedEnd), [expectedEnd]);
const maybeExpectedEndText = (() => { const maybeExpectedEndText = (() => {
@@ -169,7 +169,7 @@ export function MetadataTimes() {
function GroupTimes() { function GroupTimes() {
const { clock, mode, groupExpectedEnd, actualGroupStart, currentDay, playback } = useGroupTimerOverView(); const { clock, mode, groupExpectedEnd, actualGroupStart, currentDay, playback } = useGroupTimerOverView();
const { currentGroupId } = useCurrentGroupId(); const currentGroupId = useCurrentGroupId();
const group = useEntry(currentGroupId) as OntimeGroup | null; const group = useEntry(currentGroupId) as OntimeGroup | null;
const active = isPlaybackActive(playback); const active = isPlaybackActive(playback);
@@ -298,7 +298,7 @@ export function OffsetOverview() {
} }
export function ClockOverview({ shouldFormat, className }: OverviewTimeElementsProps & { className?: string }) { export function ClockOverview({ shouldFormat, className }: OverviewTimeElementsProps & { className?: string }) {
const { clock } = useClock(); const clock = useClock();
const formattedClock = shouldFormat ? formatTime(clock) : millisToString(clock); const formattedClock = shouldFormat ? formatTime(clock) : millisToString(clock);
return ( return (
@@ -32,6 +32,8 @@ export default function RundownEntry({
totalGap, totalGap,
isLinkedToLoaded, isLinkedToLoaded,
}: RundownEntryProps) { }: RundownEntryProps) {
'use memo';
if (isOntimeEvent(data)) { if (isOntimeEvent(data)) {
return ( return (
<RundownEvent <RundownEvent
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useMemo } from 'react';
import { import {
isOntimeDelay, isOntimeDelay,
isOntimeEvent, isOntimeEvent,
@@ -24,27 +24,22 @@ export default function RundownEntryEditor() {
const selectedEvents = useEventSelection((state) => state.selectedEvents); const selectedEvents = useEventSelection((state) => state.selectedEvents);
const { data } = useRundown(); const { data } = useRundown();
const [entry, setEntry] = useState<OntimeEvent | OntimeGroup | OntimeMilestone | null>(null); const entry = useMemo<OntimeEvent | OntimeGroup | OntimeMilestone | null>(() => {
useEffect(() => {
if (data.order.length === 0) { if (data.order.length === 0) {
setEntry(null); return null;
return;
} }
const selectedEventId = Array.from(selectedEvents).at(0); const selectedEventId = Array.from(selectedEvents).at(0);
if (!selectedEventId) { if (!selectedEventId) {
setEntry(null); return null;
return;
} }
const event = data.entries[selectedEventId]; const event = data.entries[selectedEventId];
if (event && !isOntimeDelay(event)) { if (event && !isOntimeDelay(event)) {
setEntry(event); return event;
} else {
setEntry(null);
} }
}, [data.order, data.entries, selectedEvents]); return null;
}, [data.order.length, data.entries, selectedEvents]);
if (!entry) { if (!entry) {
return <EventEditorEmpty />; return <EventEditorEmpty />;
@@ -17,6 +17,8 @@ interface RundownDelayProps {
} }
export default function RundownDelay({ data, hasCursor }: RundownDelayProps) { export default function RundownDelay({ data, hasCursor }: RundownDelayProps) {
'use memo';
const { applyDelay, deleteEntry } = useEntryActions(); const { applyDelay, deleteEntry } = useEntryActions();
const handleRef = useRef<null | HTMLSpanElement>(null); const handleRef = useRef<null | HTMLSpanElement>(null);
@@ -91,10 +91,21 @@ export default function RundownEvent({
isLinkedToLoaded, isLinkedToLoaded,
hasTriggers, hasTriggers,
}: RundownEventProps) { }: RundownEventProps) {
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping(); 'use memo';
const selectedEventId = useEventIdSwapping((state) => state.selectedEventId);
const setSelectedEventId = useEventIdSwapping((state) => state.setSelectedEventId);
const clearSelectedEventId = useEventIdSwapping((state) => state.clearSelectedEventId);
const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents } = useEntryActions(); const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents } = useEntryActions();
const { selectedEvents, unselect, setSelectedEvents, clearSelectedEvents } = useEventSelection(); const isSelected = useEventSelection((state) => state.selectedEvents.has(eventId));
const unselect = useEventSelection((state) => state.unselect);
const setSelectedEvents = useEventSelection((state) => state.setSelectedEvents);
const clearSelectedEvents = useEventSelection((state) => state.clearSelectedEvents);
const selectedEvents = useEventSelection((state) => state.selectedEvents);
const handleRef = useRef<null | HTMLSpanElement>(null); const handleRef = useRef<null | HTMLSpanElement>(null);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
@@ -250,7 +261,6 @@ export default function RundownEvent({
}; };
}, [handleRef]); }, [handleRef]);
const isSelected = selectedEvents.has(eventId);
const blockClasses = cx([ const blockClasses = cx([
style.rundownEvent, style.rundownEvent,
skip ? style.skip : null, skip ? style.skip : null,
@@ -35,7 +35,7 @@ export default function RundownEventChip({
duration, duration,
isLinkedToLoaded, isLinkedToLoaded,
}: RundownEventChipProps) { }: RundownEventChipProps) {
const { playback } = usePlayback(); const playback = usePlayback();
if (isLoaded) { if (isLoaded) {
return null; return null;
@@ -33,9 +33,13 @@ interface RundownGroupProps {
//TODO: the group should maybe include a multiple day indicator //TODO: the group should maybe include a multiple day indicator
export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }: RundownGroupProps) { export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }: RundownGroupProps) {
'use memo';
const handleRef = useRef<null | HTMLSpanElement>(null); const handleRef = useRef<null | HTMLSpanElement>(null);
const { clone, ungroup, deleteEntry } = useEntryActions(); const { clone, ungroup, deleteEntry } = useEntryActions();
const { selectedEvents, setSingleEntrySelection } = useEventSelection();
const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection);
const selectedEvents = useEventSelection((state) => state.selectedEvents);
const [onContextMenu] = useContextMenu<HTMLDivElement>([ const [onContextMenu] = useContextMenu<HTMLDivElement>([
{ {
@@ -24,7 +24,7 @@ export default memo(RundownHeader);
function RundownHeader({ viewMode, setViewMode }: RundownHeaderProps) { function RundownHeader({ viewMode, setViewMode }: RundownHeaderProps) {
const [editorMode, setEditorMode] = useSessionStorage({ key: sessionKeys.editorMode, defaultValue: AppMode.Edit }); const [editorMode, setEditorMode] = useSessionStorage({ key: sessionKeys.editorMode, defaultValue: AppMode.Edit });
const { offsetMode } = useOffsetMode(); const offsetMode = useOffsetMode();
const toggleAppMode = (mode: AppMode[]) => { const toggleAppMode = (mode: AppMode[]) => {
// we need to stop user from deselecting a mode // we need to stop user from deselecting a mode
@@ -18,7 +18,7 @@ function RundownHeader() {
defaultValue: AppMode.Edit, defaultValue: AppMode.Edit,
}); });
const { offsetMode } = useOffsetMode(); const offsetMode = useOffsetMode();
const toggleAppMode = (mode: AppMode[]) => { const toggleAppMode = (mode: AppMode[]) => {
// we need to stop user from deselecting a mode // we need to stop user from deselecting a mode
@@ -22,9 +22,13 @@ interface RundownMilestoneProps {
} }
export default function RundownMilestone({ colour, cue, entryId, hasCursor, title }: RundownMilestoneProps) { export default function RundownMilestone({ colour, cue, entryId, hasCursor, title }: RundownMilestoneProps) {
'use memo';
const handleRef = useRef<null | HTMLSpanElement>(null); const handleRef = useRef<null | HTMLSpanElement>(null);
const { updateEntry, deleteEntry } = useEntryActions(); const { updateEntry, deleteEntry } = useEntryActions();
const { selectedEvents, setSingleEntrySelection } = useEventSelection();
const selectedEvents = useEventSelection((state) => state.selectedEvents);
const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection);
const [onContextMenu] = useContextMenu<HTMLDivElement>([ const [onContextMenu] = useContextMenu<HTMLDivElement>([
{ {
@@ -215,7 +215,7 @@ function ExtraInfo({ projectData, size, source }: ExtraInfoProps) {
function BackstageClock() { function BackstageClock() {
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
const { clock } = useClock(); const clock = useClock();
// gather timer data // gather timer data
const formattedClock = formatTime(clock); const formattedClock = formatTime(clock);
@@ -141,7 +141,7 @@ function CountdownContents({ playableEvents, subscriptions, goToEditMode }: Coun
function CountdownClock() { function CountdownClock() {
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
const { clock } = useClock(); const clock = useClock();
// gather timer data // gather timer data
const formattedClock = formatTime(clock); const formattedClock = formatTime(clock);
@@ -37,8 +37,8 @@ interface CountdownSubscriptionsProps {
export default function CountdownSubscriptions({ subscribedEvents, goToEditMode }: CountdownSubscriptionsProps) { export default function CountdownSubscriptions({ subscribedEvents, goToEditMode }: CountdownSubscriptionsProps) {
const { mainSource, secondarySource, showExpected } = useCountdownOptions(); const { mainSource, secondarySource, showExpected } = useCountdownOptions();
const { playback } = usePlayback(); const playback = usePlayback();
const { selectedEventId } = useSelectedEventId(); const selectedEventId = useSelectedEventId();
const showFab = useFadeOutOnInactivity(true); const showFab = useFadeOutOnInactivity(true);
const { data: reportData } = useReport(); const { data: reportData } = useReport();
@@ -42,7 +42,7 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues
const hideTableSeconds = useOptions((state) => state.hideTableSeconds); const hideTableSeconds = useOptions((state) => state.hideTableSeconds);
const hideIndexColumn = useOptions((state) => state.hideIndexColumn); const hideIndexColumn = useOptions((state) => state.hideIndexColumn);
const { selectedEventId } = useSelectedEventId(); const selectedEventId = useSelectedEventId();
const virtuosoRef = useRef<TableVirtuosoHandle | null>(null); const virtuosoRef = useRef<TableVirtuosoHandle | null>(null);
const { listeners } = useTableNav(); const { listeners } = useTableNav();
@@ -36,7 +36,7 @@ export default function TimelinePageLoader() {
} }
function TimelinePage({ events, customFields, projectData, settings }: TimelineData) { function TimelinePage({ events, customFields, projectData, settings }: TimelineData) {
const { selectedEventId } = useSelectedEventId(); const selectedEventId = useSelectedEventId();
const { mainSource } = useTimelineOptions(); const { mainSource } = useTimelineOptions();
// holds copy of the rundown with only relevant events // holds copy of the rundown with only relevant events
const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedEventId); const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedEventId);
@@ -73,7 +73,7 @@ function TimelinePage({ events, customFields, projectData, settings }: TimelineD
function TimelineClock() { function TimelineClock() {
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
const { clock } = useClock(); const clock = useClock();
// gather timer data // gather timer data
const formattedClock = formatTime(clock); const formattedClock = formatTime(clock);