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