Files
dsh-context/tests/client/components/useRowWindow.spec.ts
T
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

138 lines
5.7 KiB
TypeScript

import assert from 'node:assert/strict'
import { createElement as h, useRef } from 'react'
import { act } from 'react'
import { describe, test, vi } from 'vitest'
import { ROW_WINDOW_OVERSCAN, rowWindowFor, useRowWindow } from '../../../src/client/components/useRowWindow'
import { mount, query, until } from '../helpers/kit'
describe('rowWindowFor', () => {
test('covers the viewport with an overscan on both sides', () => {
assert.deepEqual(rowWindowFor(0, 100, 20, 50), { from: 0, to: 5 + ROW_WINDOW_OVERSCAN })
assert.deepEqual(rowWindowFor(200, 100, 20, 50), { from: 10 - ROW_WINDOW_OVERSCAN, to: 15 + ROW_WINDOW_OVERSCAN })
assert.deepEqual(rowWindowFor(10_000, 100, 20, 50), { from: 49, to: 49 })
})
test('falls back to the whole list when unmeasured or empty', () => {
assert.deepEqual(rowWindowFor(0, 0, 20, 30), { from: 0, to: 29 })
assert.deepEqual(rowWindowFor(0, 100, 0, 30), { from: 0, to: 29 })
assert.deepEqual(rowWindowFor(0, 100, 20, 0), { from: 0, to: 0 })
})
})
function Probe(props: { count: number; enabled: boolean; attachRef?: boolean }) {
const ref = useRef<HTMLDivElement | null>(null)
const range = useRowWindow(ref, props.count, 20, props.enabled)
return h('div', {
...(props.attachRef === false ? {} : { ref }),
'data-from': range.from,
'data-to': range.to,
})
}
function NoRefProbe(props: { count: number }) {
const range = useRowWindow(useRef<HTMLDivElement | null>(null), props.count, 20, true)
return h('div', { 'data-from': range.from, 'data-to': range.to })
}
describe('useRowWindow', () => {
test('renders in full until the scroller is measured, then follows scroll', async () => {
const m = await mount(h(Probe, { count: 50, enabled: true }))
try {
const el = query<HTMLElement>(m.container, 'div')
assert.equal(el.dataset.from, '0')
assert.equal(el.dataset.to, '49')
Object.defineProperty(el, 'clientHeight', { configurable: true, value: 100 })
// The effect re-reads the box when the list changes.
await m.update(h(Probe, { count: 51, enabled: true }))
await until(() => query<HTMLElement>(m.container, 'div').dataset.to !== '50', 'window measures the viewport')
assert.equal(el.dataset.from, '0')
assert.equal(el.dataset.to, String(5 + ROW_WINDOW_OVERSCAN))
el.scrollTop = 200
await act(async () => { el.dispatchEvent(new Event('scroll')) })
await until(() => query<HTMLElement>(m.container, 'div').dataset.from !== '0', 'window follows the scroll')
assert.equal(el.dataset.from, String(10 - ROW_WINDOW_OVERSCAN))
} finally {
await m.unmount()
}
})
test('stays full when disabled, unmeasured, or the ref is not attached', async () => {
const cases = [
h(Probe, { count: 40, enabled: false }),
h(Probe, { count: 40, enabled: true, attachRef: false }),
h(NoRefProbe, { count: 40 }),
]
for (const el of cases) {
const m = await mount(el)
const node = query<HTMLElement>(m.container, 'div')
assert.equal(node.dataset.from, '0')
assert.equal(node.dataset.to, '39')
await m.unmount()
}
})
test('coalesces two scroll events into one frame and applies the latest offset', async () => {
const frames: FrameRequestCallback[] = []
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { frames.push(callback); return 1 })
vi.stubGlobal('cancelAnimationFrame', () => {})
try {
const m = await mount(h(Probe, { count: 50, enabled: true }))
const el = query<HTMLElement>(m.container, 'div')
Object.defineProperty(el, 'clientHeight', { configurable: true, value: 100 })
await m.update(h(Probe, { count: 51, enabled: true }))
await until(() => query<HTMLElement>(m.container, 'div').dataset.to !== '50', 'window measured')
await act(async () => {
el.scrollTop = 100
el.dispatchEvent(new Event('scroll'))
el.scrollTop = 200
el.dispatchEvent(new Event('scroll'))
})
assert.equal(frames.length, 1, 'one frame serves both scroll events')
await act(async () => { frames[0](0) })
await until(() => query<HTMLElement>(m.container, 'div').dataset.from !== '0', 'latest offset applied')
assert.equal(el.dataset.from, String(10 - ROW_WINDOW_OVERSCAN))
await m.unmount()
} finally {
vi.unstubAllGlobals()
}
})
test('observes pane resizes when ResizeObserver exists', async () => {
const disconnect = vi.fn()
class FakeObserver {
constructor(private readonly cb: () => void) {}
observe = (): void => { this.cb() }
disconnect = disconnect
}
vi.stubGlobal('ResizeObserver', FakeObserver)
try {
const m = await mount(h(Probe, { count: 20, enabled: true }))
const el = query<HTMLElement>(m.container, 'div')
Object.defineProperty(el, 'clientHeight', { configurable: true, value: 100 })
await m.update(h(Probe, { count: 21, enabled: true }))
await m.unmount()
assert.ok(disconnect.mock.calls.length > 0)
} finally {
vi.unstubAllGlobals()
}
})
test('cancels a pending animation frame on unmount', async () => {
const cancel = vi.fn()
vi.stubGlobal('requestAnimationFrame', () => 7)
vi.stubGlobal('cancelAnimationFrame', cancel)
try {
const m = await mount(h(Probe, { count: 50, enabled: true }))
const el = query<HTMLElement>(m.container, 'div')
Object.defineProperty(el, 'clientHeight', { configurable: true, value: 100 })
await m.update(h(Probe, { count: 51, enabled: true }))
await act(async () => { el.dispatchEvent(new Event('scroll')) })
await m.unmount()
assert.ok(cancel.mock.calls.length > 0)
} finally {
vi.unstubAllGlobals()
}
})
})