perf: dedupe the overview poll and index the jobs rollup
Measured on the deployed console: the page shell and every module land in 39ms, so the 1.4s was entirely /admin/api/ops/overview, fetched twice. Twice because the sidebar and the overview view each called usePoll on the same url, each with its own timer. usePoll now keeps one store per url, so any number of subscribers share a single request and a single interval, and a request already on the wire is joined rather than duplicated. The endpoint itself was dominated by the jobs rollup: a full scan of autonomy_jobs, 521k rows and growing about nine thousand a day. A covering index on (job_type, lane, status, created_at) takes it from 548ms to 110ms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnNxwxfXSbeNtjvtz5gayb
This commit is contained in:
@@ -46,40 +46,65 @@ export async function api(path, options = {}) {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll without the spinner flash: keep showing the previous payload while the next
|
// One store per url, shared by every component asking for it. The sidebar and the
|
||||||
// one is in flight, and never let a slow response overwrite a newer one.
|
// overview both want /ops/overview, and without this they each opened their own
|
||||||
export function usePoll(path, intervalMs = 5000) {
|
// request on their own timer -- the same payload fetched twice, forever.
|
||||||
const [state, setState] = useState({ data: null, error: null, loading: true, at: null });
|
const stores = new Map();
|
||||||
const seq = useRef(0);
|
|
||||||
const alive = useRef(true);
|
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
function storeFor(path) {
|
||||||
const mine = ++seq.current;
|
if (stores.has(path)) return stores.get(path);
|
||||||
try {
|
const store = {
|
||||||
const data = await api(path);
|
state: { data: null, error: null, loading: true, at: null },
|
||||||
if (!alive.current || mine !== seq.current) return;
|
listeners: new Set(),
|
||||||
setState({ data, error: null, loading: false, at: Date.now() });
|
inflight: null,
|
||||||
} catch (error) {
|
timer: null,
|
||||||
if (!alive.current || mine !== seq.current) return;
|
emit() { for (const fn of this.listeners) fn(this.state); },
|
||||||
console.error('[ops] poll failed for', path, error.message);
|
async load() {
|
||||||
setState((prev) => ({ ...prev, error: error.message, loading: false }));
|
// a request already on the wire is shared rather than duplicated
|
||||||
}
|
if (this.inflight) return this.inflight;
|
||||||
}, [path]);
|
this.inflight = api(path)
|
||||||
|
.then((data) => { this.state = { data, error: null, loading: false, at: Date.now() }; })
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('[ops] poll failed for', path, error.message);
|
||||||
|
this.state = { ...this.state, error: error.message, loading: false };
|
||||||
|
})
|
||||||
|
.finally(() => { this.inflight = null; this.emit(); });
|
||||||
|
return this.inflight;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
stores.set(path, store);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePoll(path, intervalMs = 5000) {
|
||||||
|
const store = storeFor(path);
|
||||||
|
const [state, setState] = useState(store.state);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
alive.current = true;
|
const listener = (next) => setState(next);
|
||||||
refresh();
|
store.listeners.add(listener);
|
||||||
const timer = setInterval(refresh, intervalMs);
|
setState(store.state);
|
||||||
const onVisible = () => { if (!document.hidden) refresh(); };
|
store.load();
|
||||||
document.addEventListener('visibilitychange', onVisible);
|
|
||||||
return () => {
|
|
||||||
alive.current = false;
|
|
||||||
clearInterval(timer);
|
|
||||||
document.removeEventListener('visibilitychange', onVisible);
|
|
||||||
};
|
|
||||||
}, [refresh, intervalMs]);
|
|
||||||
|
|
||||||
return { ...state, refresh };
|
// the interval belongs to the store, not the component, so N subscribers still
|
||||||
|
// produce exactly one request per tick
|
||||||
|
if (!store.timer) {
|
||||||
|
store.timer = setInterval(() => { if (!document.hidden) store.load(); }, intervalMs);
|
||||||
|
}
|
||||||
|
const onVisible = () => { if (!document.hidden) store.load(); };
|
||||||
|
document.addEventListener('visibilitychange', onVisible);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
store.listeners.delete(listener);
|
||||||
|
document.removeEventListener('visibilitychange', onVisible);
|
||||||
|
if (!store.listeners.size && store.timer) {
|
||||||
|
clearInterval(store.timer);
|
||||||
|
store.timer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [store, intervalMs]);
|
||||||
|
|
||||||
|
return { ...state, refresh: () => store.load() };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useHashRoute(fallback) {
|
export function useHashRoute(fallback) {
|
||||||
|
|||||||
@@ -244,6 +244,12 @@ function initAutonomySchema(db) {
|
|||||||
'ALTER TABLE autonomy_calibration_snapshots ADD COLUMN replay_run_id INTEGER',
|
'ALTER TABLE autonomy_calibration_snapshots ADD COLUMN replay_run_id INTEGER',
|
||||||
'ALTER TABLE autonomy_calibration_snapshots ADD COLUMN distinct_instruments INTEGER',
|
'ALTER TABLE autonomy_calibration_snapshots ADD COLUMN distinct_instruments INTEGER',
|
||||||
'ALTER TABLE autonomy_calibration_snapshots ADD COLUMN top_instrument_share REAL',
|
'ALTER TABLE autonomy_calibration_snapshots ADD COLUMN top_instrument_share REAL',
|
||||||
|
// The dashboard groups jobs by type/lane/status on every poll. autonomy_jobs is
|
||||||
|
// half a million rows and grows by roughly nine thousand a day from the archive
|
||||||
|
// reconcile loop, so without this that one query was 548ms and rising.
|
||||||
|
// created_at is included to keep the MAX() index-only.
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_autonomy_jobs_group ON autonomy_jobs(job_type, lane, status, created_at)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_calibration_snapshot_latest ON autonomy_calibration_snapshots(cohort_key, source, id)',
|
||||||
]) {
|
]) {
|
||||||
try {
|
try {
|
||||||
db.exec(statement);
|
db.exec(statement);
|
||||||
|
|||||||
Reference in New Issue
Block a user