Files
Bowen Liang a694adfb34 perf(context): cut context-tab render, hover and DNA costs (#167)
* perf(context): cut context-tab render, hover and DNA costs

P0 — interaction/caching:
- isolate pointer hovers: chart-local hover stays immediate, cross-card previews
  trail (140ms; 220ms in DNA mode) so sweeps cannot stack full-view renders;
- cache pure projections (assemble, DNA/file derivations) across hover/remount;
- remove forced layout from the flow/timing read paths, gate the hidden sidebar
  copy, memoize picker options and tool tallies.

P1 — windowing/lazy rendering:
- window trend bars, file-activity rows, timing-strip segments (dense gradient)
  and the browser DNA feed; off-screen rows use content-visibility;
- lazy-mount the browser card and the below-fold event/file row behind
  height-matched skeletons, mounted on an idle slice / near the viewport;
- window DNA bands on long total-mode logs, tighten the DNA window, sample
  per-bar gradient stops, and keep DNA+delta eager so its churn axis stays exact;
- force-mount + reveal for stats file-pill jumps into the lazy row.

Rendering-only by construction: axes, tooltips, ordering, DNA hit-testing and
every readout still come from the full model; scrolling only changes which rows
are in the DOM. Measured on a 987-step session: mount 986-1251ms -> ~510ms,
first paint 480-592ms -> ~80ms, zero long tasks; tab DOM 15,929 -> 3,226
(below-fold mounted ~4.7k); hover 30 -> 0 long tasks; DNA toggle 875 -> ~230ms.

Tests: new window/lazy/DNA/cache specs; lint, typecheck and 138-file 100%
per-file coverage green.

* fix(context): second-review follow-ups

- keep DNA bands fresh per header epoch, and force eager DNA+delta on long
  logs so per-key churn axes stay exact;
- correct windowed spacer heights for the container flex gap: the file list
  and the DNA feed now reproduce the full scroll height exactly;
- re-translate memoized picker/tooltip labels when the locale changes;
- re-observe lazy mounts across detach/reattach (a collapsed sidebar copy)
  via a callback ref, keep forced mounts for stats jumps, and clear the
  chart's local hover/DNA state on mouse leave;
- shrink the window for a mount that already has DNA on, and add the async
  subagent count to the flow-measurement deps;
- repair the skeleton background rule and restore the accidentally
  globalized .lc-heat-ctl .lc-card-sub selector.
2026-10-19 07:38:56 +08:00

39 lines
1.5 KiB
TypeScript

import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
import { assembleCached, cachedByData } from '../../../src/client/components/derivedCache'
import type { ContextHeaders } from '../../../src/shared/types'
import { timeline } from './contextViewHarness'
const headers: ContextHeaders = {
headers: [{ seq: 1, time: 1, systemTokens: 0, tools: [] }],
}
describe('derived cache', () => {
test('cachedByData reuses a value per projection and key', () => {
const data = timeline()
let calls = 0
const compute = () => { calls++; return { calls } }
const first = cachedByData(data, 'k', compute)
const again = cachedByData(data, 'k', compute)
assert.equal(first, again)
assert.equal(calls, 1)
const other = cachedByData(data, 'other', compute)
assert.notEqual(first, other)
assert.equal(calls, 2)
const otherData = timeline()
assert.notEqual(cachedByData(otherData, 'k', compute), first)
assert.equal(calls, 3)
})
test('assembleCached reuses one assembled view per projection, headers and step', () => {
const data = timeline()
const live = assembleCached(data, null, null)
assert.equal(assembleCached(data, null, null), live)
const withHeaders = assembleCached(data, headers, null)
assert.notEqual(withHeaders, live)
assert.equal(assembleCached(data, headers, null), withHeaders)
const otherHeaders: ContextHeaders = { headers: [] }
assert.notEqual(assembleCached(data, otherHeaders, null), withHeaders)
})
})