mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 15:09:10 +00:00
perf(cuesheet): add temporary first-render metrics scaffold
Adds a dev-only, self-contained instrumentation module under common/devtools/cuesheet-metrics to measure the first-render (mount) cost of virtualised cuesheet rows, so scroll-performance changes can be backed by before/after numbers. - Console-only API on window.__cuesheetPerf (deterministic scroll benchmark + manual start/stop), gated by isDev && ?perf=1, runtime-inert in prod. - Whole-row mount probe plus per-subsystem attribution: colour calc, getVisibleCells, AutoTextarea autosize, Tooltip portal mount, useReactiveTextInput hotkey-handler init; FPS/long-frame timing. - All edits to existing files tagged // PERF-METRICS for a one-pass teardown. Temporary: to be deleted once the optimizations are proven.
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
import autosize from 'autosize/dist/autosize';
|
import autosize from 'autosize/dist/autosize';
|
||||||
import { RefObject, useEffect } from 'react';
|
import { RefObject, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { timeSync } from '../../../devtools/cuesheet-metrics/usePerfMark'; // PERF-METRICS
|
||||||
import Textarea, { type TextareaProps } from '../textarea/Textarea';
|
import Textarea, { type TextareaProps } from '../textarea/Textarea';
|
||||||
|
|
||||||
interface AutoTextAreaProps extends TextareaProps {
|
interface AutoTextAreaProps extends TextareaProps {
|
||||||
@@ -15,7 +16,7 @@ export function AutoTextarea({ value, inputref, ...textAreaProps }: AutoTextArea
|
|||||||
// when the value changes, we use the ref to reapply autosize
|
// when the value changes, we use the ref to reapply autosize
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const node = inputref.current;
|
const node = inputref.current;
|
||||||
autosize(inputref.current);
|
timeSync('cell.autosize', () => autosize(inputref.current)); // PERF-METRICS
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
autosize.destroy(node);
|
autosize.destroy(node);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { HotkeyItem, getHotkeyHandler } from '@mantine/hooks';
|
import { HotkeyItem, getHotkeyHandler } from '@mantine/hooks';
|
||||||
import { ChangeEvent, KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { ChangeEvent, KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { timeSync } from '../../../devtools/cuesheet-metrics/usePerfMark'; // PERF-METRICS
|
||||||
|
|
||||||
interface UseReactiveTextInputReturn {
|
interface UseReactiveTextInputReturn {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||||
@@ -95,7 +97,7 @@ export default function useReactiveTextInput(
|
|||||||
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before blurring
|
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before blurring
|
||||||
}, [initialText, options, ref]);
|
}, [initialText, options, ref]);
|
||||||
|
|
||||||
const keyHandler = useMemo(() => {
|
const keyHandler = useMemo(() => timeSync('cell.reactiveInputInit', () => { // PERF-METRICS
|
||||||
const hotKeys: HotkeyItem[] = [
|
const hotKeys: HotkeyItem[] = [
|
||||||
[
|
[
|
||||||
'Escape',
|
'Escape',
|
||||||
@@ -155,7 +157,7 @@ export default function useReactiveTextInput(
|
|||||||
|
|
||||||
hotKeyHandler(event);
|
hotKeyHandler(event);
|
||||||
};
|
};
|
||||||
}, [handleEscape, handleSubmit, options?.submitOnCtrlEnter, options?.submitOnEnter, options?.submitOnTab, text]);
|
}), [handleEscape, handleSubmit, options?.submitOnCtrlEnter, options?.submitOnEnter, options?.submitOnTab, text]); // PERF-METRICS
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: text,
|
value: text,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
|
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { useMountProbe } from '../../devtools/cuesheet-metrics/usePerfMark'; // PERF-METRICS
|
||||||
|
|
||||||
import style from './Tooltip.module.scss';
|
import style from './Tooltip.module.scss';
|
||||||
|
|
||||||
interface TooltipProps extends BaseTooltip.Trigger.Props {
|
interface TooltipProps extends BaseTooltip.Trigger.Props {
|
||||||
@@ -8,6 +10,7 @@ interface TooltipProps extends BaseTooltip.Trigger.Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Tooltip({ text, children, ...triggerProps }: PropsWithChildren<TooltipProps>) {
|
export default function Tooltip({ text, children, ...triggerProps }: PropsWithChildren<TooltipProps>) {
|
||||||
|
useMountProbe('cell.tooltip'); // PERF-METRICS
|
||||||
return (
|
return (
|
||||||
<BaseTooltip.Root>
|
<BaseTooltip.Root>
|
||||||
<BaseTooltip.Trigger {...triggerProps}>{children}</BaseTooltip.Trigger>
|
<BaseTooltip.Trigger {...triggerProps}>{children}</BaseTooltip.Trigger>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# cuesheet-metrics (TEMPORARY)
|
||||||
|
|
||||||
|
Dev-only instrumentation to measure the **first-render (mount) cost of virtualised cuesheet
|
||||||
|
rows**, so the scroll-performance optimizations can be supported by before/after numbers.
|
||||||
|
|
||||||
|
> This whole directory is temporary and must be deleted once the optimizations are proven.
|
||||||
|
> Every edit it makes to existing files is tagged `// PERF-METRICS`.
|
||||||
|
|
||||||
|
## Enable
|
||||||
|
|
||||||
|
Open the cuesheet in a **dev build** with the `?perf=1` query param:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:3000/cuesheet?perf=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing is collected and nothing is exposed unless both conditions hold (`isDev && ?perf=1`).
|
||||||
|
In production builds `isDev` is `false`, so all recording paths are runtime-inert.
|
||||||
|
|
||||||
|
## Measure
|
||||||
|
|
||||||
|
A console API is exposed on `window.__cuesheetPerf`:
|
||||||
|
|
||||||
|
- `runScrollBenchmark(options?)` — deterministic, hands-off scroll from `fromIndex` to
|
||||||
|
`toIndex` and back, then prints the result. Use the same options before/after a change so
|
||||||
|
numbers are comparable. Options: `{ fromIndex=0, toIndex=200, stride=8, stepMs=16 }`.
|
||||||
|
- `start()` / `stop()` — begin a manual session, scroll by hand, then stop to print.
|
||||||
|
- `dump()` / `reset()`.
|
||||||
|
|
||||||
|
To emulate a low-end device, set Chrome DevTools → Performance → CPU 4×–6× slowdown, then run
|
||||||
|
the benchmark 3× and compare medians.
|
||||||
|
|
||||||
|
## What is reported
|
||||||
|
|
||||||
|
- **Summary:** rows mounted, frames, long frames (>~25ms), avg/min FPS, worst frame ms.
|
||||||
|
- **Marks** (count / avg ms / max ms / total ms):
|
||||||
|
- `EventRow.mount` — whole-row mount time (render-start → post-commit layout)
|
||||||
|
- `cell.tooltip.mount` — mount of a delay-indicator tooltip subtree (portal/positioner)
|
||||||
|
- `row.colourCalc` — per-row colour-accessibility computation
|
||||||
|
- `row.getVisibleCells` — react-table visible-cell expansion
|
||||||
|
- `cell.autosize` — `AutoTextarea` autosize + MutationObserver setup
|
||||||
|
- `cell.reactiveInputInit` — `useReactiveTextInput` hotkey-handler construction
|
||||||
|
- `longtask` — main-thread long tasks captured during the session (where supported)
|
||||||
|
|
||||||
|
## Teardown
|
||||||
|
|
||||||
|
1. `rg "PERF-METRICS" apps/client/src` → revert each tagged line in the existing files
|
||||||
|
(`CuesheetTable.tsx`, `EventRow.tsx`, `AutoTextarea.tsx`, `Tooltip.tsx`,
|
||||||
|
`useReactiveTextInput.tsx`) back to its original body.
|
||||||
|
2. Delete this directory.
|
||||||
|
3. `rg "cuesheet-metrics|__cuesheetPerf" apps/client/src` → must be empty.
|
||||||
|
4. `pnpm typecheck && pnpm test:pipeline && pnpm build`.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* PERF-METRICS — temporary cuesheet scroll-performance scaffold (delete with this directory).
|
||||||
|
*
|
||||||
|
* A deterministic, hands-off scroll runner so before/after numbers are comparable. Scrolls the
|
||||||
|
* virtualised table from `fromIndex` to `toIndex` and back in fixed index strides, under an
|
||||||
|
* active frame-timing session, then dumps the result.
|
||||||
|
*/
|
||||||
|
import type { RefObject } from 'react';
|
||||||
|
import type { TableVirtuosoHandle } from 'react-virtuoso';
|
||||||
|
|
||||||
|
import { PERF_ENABLED } from './perfConfig';
|
||||||
|
import { dump, endSession, startSession } from './perfStore';
|
||||||
|
import { startFpsMonitor, stopFpsMonitor } from './scrollFpsMonitor';
|
||||||
|
|
||||||
|
export interface BenchmarkOptions {
|
||||||
|
fromIndex?: number;
|
||||||
|
toIndex?: number;
|
||||||
|
/** Indices advanced per step. */
|
||||||
|
stride?: number;
|
||||||
|
/** Delay between steps, ms. */
|
||||||
|
stepMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runScrollBenchmark(
|
||||||
|
virtuosoRef: RefObject<TableVirtuosoHandle | null>,
|
||||||
|
options: BenchmarkOptions = {},
|
||||||
|
): Promise<void> {
|
||||||
|
if (!PERF_ENABLED) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('[cuesheet-perf] disabled — open the cuesheet with ?perf=1 in a dev build.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handle = virtuosoRef.current;
|
||||||
|
if (!handle) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('[cuesheet-perf] virtuoso handle not ready.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fromIndex = 0, toIndex = 200, stride = 8, stepMs = 16 } = options;
|
||||||
|
|
||||||
|
startSession();
|
||||||
|
startFpsMonitor();
|
||||||
|
|
||||||
|
await scrollThrough(handle, fromIndex, toIndex, stride, stepMs);
|
||||||
|
await scrollThrough(handle, toIndex, fromIndex, stride, stepMs);
|
||||||
|
|
||||||
|
stopFpsMonitor();
|
||||||
|
endSession();
|
||||||
|
dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollThrough(
|
||||||
|
handle: TableVirtuosoHandle,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
stride: number,
|
||||||
|
stepMs: number,
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const dir = to >= from ? 1 : -1;
|
||||||
|
let current = from;
|
||||||
|
|
||||||
|
const step = () => {
|
||||||
|
handle.scrollToIndex({ index: current, behavior: 'auto', align: 'start' });
|
||||||
|
|
||||||
|
if (current === to) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
current += stride * dir;
|
||||||
|
if ((dir > 0 && current > to) || (dir < 0 && current < to)) {
|
||||||
|
current = to;
|
||||||
|
}
|
||||||
|
setTimeout(step, stepMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
step();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* PERF-METRICS — temporary cuesheet scroll-performance scaffold.
|
||||||
|
*
|
||||||
|
* This whole directory (common/devtools/cuesheet-metrics) is temporary and is meant to be
|
||||||
|
* deleted once the first-render optimizations are proven. See the perf investigation plan.
|
||||||
|
*
|
||||||
|
* Gates all instrumentation: enabled only in a dev build AND when the page is opened with
|
||||||
|
* `?perf=1`. Because `isDev` resolves to `import.meta.env.DEV`, this is `false` in production,
|
||||||
|
* so every recording path stays runtime-inert there (the guarded code may still be present in
|
||||||
|
* the bundle — it is never executed, and the whole scaffold is removed at teardown).
|
||||||
|
*/
|
||||||
|
import { isDev } from '../../../externals';
|
||||||
|
|
||||||
|
function readPerfParam(): boolean {
|
||||||
|
try {
|
||||||
|
return new URLSearchParams(window.location.search).get('perf') === '1';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PERF_ENABLED = isDev && readPerfParam();
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* PERF-METRICS — temporary cuesheet scroll-performance scaffold (delete with this directory).
|
||||||
|
*
|
||||||
|
* Plain module singleton (no zustand): the metrics are surfaced via the console only, so there
|
||||||
|
* is no React subscriber that would justify a reactive store. Everything here is a no-op unless
|
||||||
|
* a session is running, and call sites are additionally gated by PERF_ENABLED.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** A frame slower than ~1.5 frames at 60fps is counted as a "long frame". */
|
||||||
|
const LONG_FRAME_MS = (1000 / 60) * 1.5;
|
||||||
|
|
||||||
|
interface MarkStat {
|
||||||
|
count: number;
|
||||||
|
totalMs: number;
|
||||||
|
maxMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PerfState {
|
||||||
|
running: boolean;
|
||||||
|
marks: Record<string, MarkStat>;
|
||||||
|
rowsMounted: number;
|
||||||
|
frameCount: number;
|
||||||
|
totalFrameMs: number;
|
||||||
|
maxFrameMs: number;
|
||||||
|
longFrames: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createState(): PerfState {
|
||||||
|
return {
|
||||||
|
running: false,
|
||||||
|
marks: {},
|
||||||
|
rowsMounted: 0,
|
||||||
|
frameCount: 0,
|
||||||
|
totalFrameMs: 0,
|
||||||
|
maxFrameMs: 0,
|
||||||
|
longFrames: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = createState();
|
||||||
|
|
||||||
|
export function isRunning(): boolean {
|
||||||
|
return state.running;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startSession(): void {
|
||||||
|
state = createState();
|
||||||
|
state.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function endSession(): void {
|
||||||
|
state.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reset(): void {
|
||||||
|
state = createState();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordMark(name: string, ms: number): void {
|
||||||
|
if (!state.running) return;
|
||||||
|
const mark = state.marks[name] ?? { count: 0, totalMs: 0, maxMs: 0 };
|
||||||
|
mark.count += 1;
|
||||||
|
mark.totalMs += ms;
|
||||||
|
mark.maxMs = Math.max(mark.maxMs, ms);
|
||||||
|
state.marks[name] = mark;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordRowMount(): void {
|
||||||
|
if (!state.running) return;
|
||||||
|
state.rowsMounted += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordFrame(deltaMs: number): void {
|
||||||
|
if (!state.running) return;
|
||||||
|
state.frameCount += 1;
|
||||||
|
state.totalFrameMs += deltaMs;
|
||||||
|
state.maxFrameMs = Math.max(state.maxFrameMs, deltaMs);
|
||||||
|
if (deltaMs > LONG_FRAME_MS) {
|
||||||
|
state.longFrames += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dumps the collected metrics to the console as tables. */
|
||||||
|
export function dump(): void {
|
||||||
|
const markRows = Object.entries(state.marks)
|
||||||
|
.map(([name, m]) => ({
|
||||||
|
mark: name,
|
||||||
|
count: m.count,
|
||||||
|
avgMs: round(m.totalMs / m.count),
|
||||||
|
maxMs: round(m.maxMs),
|
||||||
|
totalMs: round(m.totalMs),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.totalMs - a.totalMs);
|
||||||
|
|
||||||
|
const avgFps = state.totalFrameMs > 0 ? round((state.frameCount * 1000) / state.totalFrameMs) : 0;
|
||||||
|
const minFps = state.maxFrameMs > 0 ? round(1000 / state.maxFrameMs) : 0;
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
rowsMounted: state.rowsMounted,
|
||||||
|
frames: state.frameCount,
|
||||||
|
longFrames: state.longFrames,
|
||||||
|
avgFps,
|
||||||
|
minFps,
|
||||||
|
maxFrameMs: round(state.maxFrameMs),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
console.group('[cuesheet-perf] benchmark result');
|
||||||
|
console.table(summary);
|
||||||
|
console.table(markRows);
|
||||||
|
console.groupEnd();
|
||||||
|
/* eslint-enable no-console */
|
||||||
|
}
|
||||||
|
|
||||||
|
function round(value: number): number {
|
||||||
|
return Math.round(value * 100) / 100;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* PERF-METRICS — temporary cuesheet scroll-performance scaffold (delete with this directory).
|
||||||
|
*
|
||||||
|
* Drives a requestAnimationFrame loop while a session is running to capture frame timing
|
||||||
|
* (FPS / long-frame count) and, where supported, records main-thread long tasks.
|
||||||
|
*/
|
||||||
|
import { PERF_ENABLED } from './perfConfig';
|
||||||
|
import { recordFrame, recordMark } from './perfStore';
|
||||||
|
|
||||||
|
let rafId: number | null = null;
|
||||||
|
let lastFrame = 0;
|
||||||
|
let observer: PerformanceObserver | null = null;
|
||||||
|
|
||||||
|
export function startFpsMonitor(): void {
|
||||||
|
if (!PERF_ENABLED || rafId !== null) return;
|
||||||
|
|
||||||
|
lastFrame = performance.now();
|
||||||
|
const tick = (now: number) => {
|
||||||
|
recordFrame(now - lastFrame);
|
||||||
|
lastFrame = now;
|
||||||
|
rafId = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
rafId = requestAnimationFrame(tick);
|
||||||
|
|
||||||
|
if (typeof PerformanceObserver !== 'undefined') {
|
||||||
|
try {
|
||||||
|
observer = new PerformanceObserver((list) => {
|
||||||
|
for (const entry of list.getEntries()) {
|
||||||
|
recordMark('longtask', entry.duration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe({ entryTypes: ['longtask'] });
|
||||||
|
} catch {
|
||||||
|
observer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopFpsMonitor(): void {
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
rafId = null;
|
||||||
|
}
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect();
|
||||||
|
observer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* PERF-METRICS — temporary cuesheet scroll-performance scaffold (delete with this directory).
|
||||||
|
*
|
||||||
|
* Wires the metrics scaffold into the cuesheet table. When enabled (`?perf=1` in a dev build) it
|
||||||
|
* exposes a console API on `window.__cuesheetPerf` and is otherwise an inert no-op.
|
||||||
|
*
|
||||||
|
* __cuesheetPerf.runScrollBenchmark() // deterministic scroll, dumps a comparable result
|
||||||
|
* __cuesheetPerf.start() // begin a manual session (then scroll by hand)
|
||||||
|
* __cuesheetPerf.stop() // end the manual session and dump
|
||||||
|
*/
|
||||||
|
import { useEffect, type RefObject } from 'react';
|
||||||
|
import type { TableVirtuosoHandle } from 'react-virtuoso';
|
||||||
|
|
||||||
|
import { runScrollBenchmark, type BenchmarkOptions } from './benchmark';
|
||||||
|
import { PERF_ENABLED } from './perfConfig';
|
||||||
|
import { dump, endSession, reset, startSession } from './perfStore';
|
||||||
|
import { startFpsMonitor, stopFpsMonitor } from './scrollFpsMonitor';
|
||||||
|
|
||||||
|
export function useCuesheetPerf(virtuosoRef: RefObject<TableVirtuosoHandle | null>): void {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!PERF_ENABLED) return;
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
runScrollBenchmark: (options?: BenchmarkOptions) => runScrollBenchmark(virtuosoRef, options),
|
||||||
|
start: () => {
|
||||||
|
startSession();
|
||||||
|
startFpsMonitor();
|
||||||
|
},
|
||||||
|
stop: () => {
|
||||||
|
stopFpsMonitor();
|
||||||
|
endSession();
|
||||||
|
dump();
|
||||||
|
},
|
||||||
|
dump,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
|
||||||
|
(window as unknown as { __cuesheetPerf?: typeof api }).__cuesheetPerf = api;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.info('[cuesheet-perf] enabled. window.__cuesheetPerf = { runScrollBenchmark, start, stop, dump, reset }');
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stopFpsMonitor();
|
||||||
|
delete (window as unknown as { __cuesheetPerf?: typeof api }).__cuesheetPerf;
|
||||||
|
};
|
||||||
|
}, [virtuosoRef]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* PERF-METRICS — temporary cuesheet scroll-performance scaffold (delete with this directory).
|
||||||
|
*
|
||||||
|
* Lightweight timing helpers. All no-op when instrumentation is disabled so the call sites in
|
||||||
|
* shared components stay cheap (and fold away in production builds).
|
||||||
|
*/
|
||||||
|
import { useLayoutEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { PERF_ENABLED } from './perfConfig';
|
||||||
|
import { recordMark, recordRowMount } from './perfStore';
|
||||||
|
|
||||||
|
/** Times the synchronous `fn`, records the duration under `name`, and returns its result. */
|
||||||
|
export function timeSync<T>(name: string, fn: () => T): T {
|
||||||
|
if (!PERF_ENABLED) return fn();
|
||||||
|
const start = performance.now();
|
||||||
|
try {
|
||||||
|
return fn();
|
||||||
|
} finally {
|
||||||
|
recordMark(name, performance.now() - start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the mount cost of a component (render-start → post-commit layout) and records it
|
||||||
|
* under `${name}.mount`. Used on EventRow and on the Tooltip subtree to attribute per-row
|
||||||
|
* first-render cost. Pass `countAsRow` on the row probe so only rows feed the rows-mounted tally.
|
||||||
|
*/
|
||||||
|
export function useMountProbe(name: string, countAsRow = false): void {
|
||||||
|
const renderStart = useRef(PERF_ENABLED ? performance.now() : 0);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!PERF_ENABLED) return;
|
||||||
|
recordMark(`${name}.mount`, performance.now() - renderStart.current);
|
||||||
|
if (countAsRow) {
|
||||||
|
recordRowMount();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only probe
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import EmptyPage from '../../../common/components/state/EmptyPage';
|
import EmptyPage from '../../../common/components/state/EmptyPage';
|
||||||
import EmptyTableBody from '../../../common/components/state/EmptyTableBody';
|
import EmptyTableBody from '../../../common/components/state/EmptyTableBody';
|
||||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||||
|
import { useCuesheetPerf } from '../../../common/devtools/cuesheet-metrics/useCuesheetPerf'; // PERF-METRICS
|
||||||
import type { RundownSource } from '../../../common/hooks-query/useScopedRundown';
|
import type { RundownSource } from '../../../common/hooks-query/useScopedRundown';
|
||||||
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||||
import { usePersistedRundownOptions } from '../../../features/rundown/rundown.options';
|
import { usePersistedRundownOptions } from '../../../features/rundown/rundown.options';
|
||||||
@@ -72,6 +73,7 @@ export default function CuesheetTable({
|
|||||||
const setScrollHandler = useEventSelection((state) => state.setScrollHandler);
|
const setScrollHandler = useEventSelection((state) => state.setScrollHandler);
|
||||||
|
|
||||||
const virtuosoRef = useRef<TableVirtuosoHandle | null>(null);
|
const virtuosoRef = useRef<TableVirtuosoHandle | null>(null);
|
||||||
|
useCuesheetPerf(virtuosoRef); // PERF-METRICS
|
||||||
const { listeners } = useTableNav();
|
const { listeners } = useTableNav();
|
||||||
|
|
||||||
const meta = useMemo(
|
const meta = useMemo(
|
||||||
|
|||||||
+21
-18
@@ -5,6 +5,7 @@ import { CSSProperties, memo, useMemo } from 'react';
|
|||||||
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
||||||
|
|
||||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||||
|
import { timeSync, useMountProbe } from '../../../../common/devtools/cuesheet-metrics/usePerfMark'; // PERF-METRICS
|
||||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||||
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||||
import { AppMode } from '../../../../ontimeConfig';
|
import { AppMode } from '../../../../ontimeConfig';
|
||||||
@@ -56,25 +57,29 @@ function EventRow({
|
|||||||
|
|
||||||
const openMenu = useCuesheetTableMenu((store) => store.openMenu);
|
const openMenu = useCuesheetTableMenu((store) => store.openMenu);
|
||||||
|
|
||||||
|
useMountProbe('EventRow', true); // PERF-METRICS
|
||||||
|
|
||||||
const { rowBgColour, backgroundColor, mutedText } = useMemo(() => {
|
const { rowBgColour, backgroundColor, mutedText } = useMemo(() => {
|
||||||
const accessible = getAccessibleColour(colour);
|
return timeSync('row.colourCalc', () => { // PERF-METRICS
|
||||||
const tmpColour = cssOrHexToColour(accessible.color) as RGBColour;
|
const accessible = getAccessibleColour(colour);
|
||||||
|
const tmpColour = cssOrHexToColour(accessible.color) as RGBColour;
|
||||||
|
|
||||||
let rowBgColour: string | undefined;
|
let rowBgColour: string | undefined;
|
||||||
if (isLoaded) {
|
if (isLoaded) {
|
||||||
rowBgColour = '#087A27'; // $active-green
|
rowBgColour = '#087A27'; // $active-green
|
||||||
} else if (colour) {
|
} else if (colour) {
|
||||||
const accessibleBg = cssOrHexToColour(accessible.backgroundColor);
|
const accessibleBg = cssOrHexToColour(accessible.backgroundColor);
|
||||||
if (accessibleBg !== null) {
|
if (accessibleBg !== null) {
|
||||||
rowBgColour = colourToHex({ ...accessibleBg, alpha: accessibleBg.alpha * 0.25 });
|
rowBgColour = colourToHex({ ...accessibleBg, alpha: accessibleBg.alpha * 0.25 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rowBgColour,
|
rowBgColour,
|
||||||
backgroundColor: accessible.backgroundColor,
|
backgroundColor: accessible.backgroundColor,
|
||||||
mutedText: colourToHex({ ...tmpColour, alpha: tmpColour.alpha * 0.8 }),
|
mutedText: colourToHex({ ...tmpColour, alpha: tmpColour.alpha * 0.8 }),
|
||||||
};
|
};
|
||||||
|
}); // PERF-METRICS
|
||||||
}, [colour, isLoaded]);
|
}, [colour, isLoaded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -117,9 +122,7 @@ function EventRow({
|
|||||||
{eventIndex}
|
{eventIndex}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{table
|
{timeSync('row.getVisibleCells', () => table.getRow(rowId).getVisibleCells()) // PERF-METRICS
|
||||||
.getRow(rowId)
|
|
||||||
.getVisibleCells()
|
|
||||||
.map((cell) => {
|
.map((cell) => {
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
|
|||||||
Reference in New Issue
Block a user