mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 14:39:06 +00:00
perf(cuesheet): add result capture + candidate lazy-autosize (Exp B, under measurement)
- Scaffold: snapshot()/getResults() so the benchmark returns structured metrics for automated before/after capture (PERF-METRICS). - Candidate Exp B: defer AutoTextarea autosize() off the mount path to focus-time, removing the per-row forced reflows during scroll. Under evaluation against the metrics.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
// @ts-expect-error no types from library
|
// @ts-expect-error no types from library
|
||||||
import autosize from 'autosize/dist/autosize';
|
import autosize from 'autosize/dist/autosize';
|
||||||
import { RefObject, useEffect } from 'react';
|
import { FocusEvent, RefObject, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { timeSync } from '../../../devtools/cuesheet-metrics/usePerfMark'; // PERF-METRICS
|
import { timeSync } from '../../../devtools/cuesheet-metrics/usePerfMark'; // PERF-METRICS
|
||||||
import Textarea, { type TextareaProps } from '../textarea/Textarea';
|
import Textarea, { type TextareaProps } from '../textarea/Textarea';
|
||||||
@@ -10,18 +10,41 @@ interface AutoTextAreaProps extends TextareaProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A textarea that automatically resizes based on its content
|
* A textarea that automatically resizes based on its content.
|
||||||
|
*
|
||||||
|
* `autosize()` forces a synchronous reflow (reads scrollHeight) and installs a MutationObserver.
|
||||||
|
* Doing that on mount makes it expensive when many of these are mounted at once (e.g. virtualised
|
||||||
|
* table rows during scroll), so we only attach autosize while the field is focused for editing and
|
||||||
|
* keep it in sync with the value during that time.
|
||||||
*/
|
*/
|
||||||
export function AutoTextarea({ value, inputref, ...textAreaProps }: AutoTextAreaProps) {
|
export function AutoTextarea({ value, inputref, onFocus, onBlur, ...textAreaProps }: AutoTextAreaProps) {
|
||||||
// when the value changes, we use the ref to reapply autosize
|
const handleFocus = useCallback(
|
||||||
|
(event: FocusEvent<HTMLTextAreaElement>) => {
|
||||||
|
timeSync('cell.autosize', () => autosize(inputref.current)); // PERF-METRICS
|
||||||
|
onFocus?.(event);
|
||||||
|
},
|
||||||
|
[inputref, onFocus],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBlur = useCallback(
|
||||||
|
(event: FocusEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (inputref.current) {
|
||||||
|
autosize.destroy(inputref.current);
|
||||||
|
}
|
||||||
|
onBlur?.(event);
|
||||||
|
},
|
||||||
|
[inputref, onBlur],
|
||||||
|
);
|
||||||
|
|
||||||
|
// while focused, keep the height in sync as the value changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const node = inputref.current;
|
const node = inputref.current;
|
||||||
timeSync('cell.autosize', () => autosize(inputref.current)); // PERF-METRICS
|
if (node && document.activeElement === node) {
|
||||||
|
autosize(node);
|
||||||
return () => {
|
}
|
||||||
autosize.destroy(node);
|
|
||||||
};
|
|
||||||
}, [inputref, value]);
|
}, [inputref, value]);
|
||||||
|
|
||||||
return <Textarea ref={inputref} value={value} {...textAreaProps} />;
|
return (
|
||||||
|
<Textarea ref={inputref} value={value} onFocus={handleFocus} onBlur={handleBlur} {...textAreaProps} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { RefObject } from 'react';
|
|||||||
import type { TableVirtuosoHandle } from 'react-virtuoso';
|
import type { TableVirtuosoHandle } from 'react-virtuoso';
|
||||||
|
|
||||||
import { PERF_ENABLED } from './perfConfig';
|
import { PERF_ENABLED } from './perfConfig';
|
||||||
import { dump, endSession, startSession } from './perfStore';
|
import { dump, endSession, snapshot, startSession, type PerfSnapshot } from './perfStore';
|
||||||
import { startFpsMonitor, stopFpsMonitor } from './scrollFpsMonitor';
|
import { startFpsMonitor, stopFpsMonitor } from './scrollFpsMonitor';
|
||||||
|
|
||||||
export interface BenchmarkOptions {
|
export interface BenchmarkOptions {
|
||||||
@@ -24,17 +24,17 @@ export interface BenchmarkOptions {
|
|||||||
export async function runScrollBenchmark(
|
export async function runScrollBenchmark(
|
||||||
virtuosoRef: RefObject<TableVirtuosoHandle | null>,
|
virtuosoRef: RefObject<TableVirtuosoHandle | null>,
|
||||||
options: BenchmarkOptions = {},
|
options: BenchmarkOptions = {},
|
||||||
): Promise<void> {
|
): Promise<PerfSnapshot | undefined> {
|
||||||
if (!PERF_ENABLED) {
|
if (!PERF_ENABLED) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn('[cuesheet-perf] disabled — open the cuesheet with ?perf=1 in a dev build.');
|
console.warn('[cuesheet-perf] disabled — open the cuesheet with ?perf=1 in a dev build.');
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
const handle = virtuosoRef.current;
|
const handle = virtuosoRef.current;
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn('[cuesheet-perf] virtuoso handle not ready.');
|
console.warn('[cuesheet-perf] virtuoso handle not ready.');
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { fromIndex = 0, toIndex = 200, stride = 8, stepMs = 16 } = options;
|
const { fromIndex = 0, toIndex = 200, stride = 8, stepMs = 16 } = options;
|
||||||
@@ -48,6 +48,7 @@ export async function runScrollBenchmark(
|
|||||||
stopFpsMonitor();
|
stopFpsMonitor();
|
||||||
endSession();
|
endSession();
|
||||||
dump();
|
dump();
|
||||||
|
return snapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollThrough(
|
function scrollThrough(
|
||||||
|
|||||||
@@ -80,9 +80,21 @@ export function recordFrame(deltaMs: number): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Dumps the collected metrics to the console as tables. */
|
export interface PerfSnapshot {
|
||||||
export function dump(): void {
|
summary: {
|
||||||
const markRows = Object.entries(state.marks)
|
rowsMounted: number;
|
||||||
|
frames: number;
|
||||||
|
longFrames: number;
|
||||||
|
avgFps: number;
|
||||||
|
minFps: number;
|
||||||
|
maxFrameMs: number;
|
||||||
|
};
|
||||||
|
marks: Array<{ mark: string; count: number; avgMs: number; maxMs: number; totalMs: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the collected metrics as a structured object (for console or automated capture). */
|
||||||
|
export function snapshot(): PerfSnapshot {
|
||||||
|
const marks = Object.entries(state.marks)
|
||||||
.map(([name, m]) => ({
|
.map(([name, m]) => ({
|
||||||
mark: name,
|
mark: name,
|
||||||
count: m.count,
|
count: m.count,
|
||||||
@@ -95,19 +107,26 @@ export function dump(): void {
|
|||||||
const avgFps = state.totalFrameMs > 0 ? round((state.frameCount * 1000) / state.totalFrameMs) : 0;
|
const avgFps = state.totalFrameMs > 0 ? round((state.frameCount * 1000) / state.totalFrameMs) : 0;
|
||||||
const minFps = state.maxFrameMs > 0 ? round(1000 / state.maxFrameMs) : 0;
|
const minFps = state.maxFrameMs > 0 ? round(1000 / state.maxFrameMs) : 0;
|
||||||
|
|
||||||
const summary = {
|
return {
|
||||||
rowsMounted: state.rowsMounted,
|
summary: {
|
||||||
frames: state.frameCount,
|
rowsMounted: state.rowsMounted,
|
||||||
longFrames: state.longFrames,
|
frames: state.frameCount,
|
||||||
avgFps,
|
longFrames: state.longFrames,
|
||||||
minFps,
|
avgFps,
|
||||||
maxFrameMs: round(state.maxFrameMs),
|
minFps,
|
||||||
|
maxFrameMs: round(state.maxFrameMs),
|
||||||
|
},
|
||||||
|
marks,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dumps the collected metrics to the console as tables. */
|
||||||
|
export function dump(): void {
|
||||||
|
const { summary, marks } = snapshot();
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.group('[cuesheet-perf] benchmark result');
|
console.group('[cuesheet-perf] benchmark result');
|
||||||
console.table(summary);
|
console.table(summary);
|
||||||
console.table(markRows);
|
console.table(marks);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { TableVirtuosoHandle } from 'react-virtuoso';
|
|||||||
|
|
||||||
import { runScrollBenchmark, type BenchmarkOptions } from './benchmark';
|
import { runScrollBenchmark, type BenchmarkOptions } from './benchmark';
|
||||||
import { PERF_ENABLED } from './perfConfig';
|
import { PERF_ENABLED } from './perfConfig';
|
||||||
import { dump, endSession, reset, startSession } from './perfStore';
|
import { dump, endSession, reset, snapshot, startSession } from './perfStore';
|
||||||
import { startFpsMonitor, stopFpsMonitor } from './scrollFpsMonitor';
|
import { startFpsMonitor, stopFpsMonitor } from './scrollFpsMonitor';
|
||||||
|
|
||||||
export function useCuesheetPerf(virtuosoRef: RefObject<TableVirtuosoHandle | null>): void {
|
export function useCuesheetPerf(virtuosoRef: RefObject<TableVirtuosoHandle | null>): void {
|
||||||
@@ -33,6 +33,7 @@ export function useCuesheetPerf(virtuosoRef: RefObject<TableVirtuosoHandle | nul
|
|||||||
},
|
},
|
||||||
dump,
|
dump,
|
||||||
reset,
|
reset,
|
||||||
|
getResults: snapshot,
|
||||||
};
|
};
|
||||||
|
|
||||||
(window as unknown as { __cuesheetPerf?: typeof api }).__cuesheetPerf = api;
|
(window as unknown as { __cuesheetPerf?: typeof api }).__cuesheetPerf = api;
|
||||||
|
|||||||
Reference in New Issue
Block a user