* 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.
138 lines
5.7 KiB
TypeScript
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()
|
|
}
|
|
})
|
|
|
|
})
|