/** * Perf lane: a MEASUREMENT harness, not a pass/fail gate (beyond the mount * sanity it shares with the other lanes). * * It exists to put NUMBERS on the sidebar's client-side cost in a real * mounted `dsh web` (the same scratch-profile boot as scripts/e2e-mount.sh): * * 1. mount latency — navigationStart → `[data-dsh-apes-sidebar]` * attached, sampled by an init-script probe (rAF loop, so it catches * the attach even before Playwright's first poll); * 2. longtasks — every >50ms main-thread task from first paint * through the full built-in tab sweep (the heaviest mount surface); * 3. bundle cost — per-resource transferSize/decodedBodySize/duration * for everything sidebar-shaped the page fetched; * 4. bottom-drag frames — rAF frame-interval p95/max while dragging the * bottom strip (the layout-push hot path), plus the width-leak guard * from the drag lane (this shell pushes no width at all: the right * column belongs to DSH's native Sidebar). * * Results print as single-line `PERF_JSON` records (plus a `PERF_SUMMARY` * aggregate) so a shell run can `tee` the log and grep them out; the * before/after comparison lives in docs/plans/2026-08-31-perf-optimization.md. * Run it 3× and take the median of each metric — single-run numbers on a * developer machine are noise, not baselines. */ import { mkdirSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { test, expect, type APIRequestContext, type Page } from '@playwright/test' import { PAGE_URL, createHostApi, hostRpc, sendFirstMessage } from './host' /** This lane's own workspace (lanes run serially against one server — never * share seed paths with mount/drag). */ const WORKSPACE_PATH = process.env.DSH_E2E_PERF_WORKSPACE ?? join(tmpdir(), 'dsh-e2e-perf-workspace') /** * Every tab type the composed guide must offer: the plugin's own four plus the * host's `terminal`. This plugin deliberately ships neither terminal nor * browser, so exactly one `terminal` entry proves it is not shadowing the host. * * `browser` is deliberately absent: DSH 0.1.7 mounts * `@deepseek-ai/dsh-client-ui-sidebar-browser` only for the desktop profile and * this lane drives the web profile, so demanding a `browser` entry would demand * something the host itself stopped offering. mount.e2e.ts asserts the absence * explicitly. The ids are the guides' `data-sidebar-right-guide-entry` keys. */ const NATIVE_TABS = ['files', 'git', 'subagent', 'sidechat', 'terminal'] as const let api: APIRequestContext async function seedSession(): Promise { mkdirSync(WORKSPACE_PATH, { recursive: true }) writeFileSync(join(WORKSPACE_PATH, 'seed.txt'), 'perf lane\n') const workspace = await hostRpc<{ workspace: { workspaceId: string } }>(api, 'workspace.create', { path: WORKSPACE_PATH }) await hostRpc(api, 'session.create', { workspaceId: workspace.value.workspace.workspaceId }) } test.beforeAll(async () => { api = await createHostApi() await seedSession() }) test.afterAll(async () => { await api?.dispose() }) /** The longtask collector + mount-attach probe, injected before ANY page * script runs (longtasks observed late are gone forever). */ const PERF_PROBE = `(() => { const perf = { mountAt: null, longtasks: [] } window.__perfProbe = perf new PerformanceObserver(list => { for (const entry of list.getEntries()) perf.longtasks.push({ start: entry.startTime, duration: entry.duration }) }).observe({ entryTypes: ['longtask'] }) const check = () => { if (document.querySelector('[data-dsh-apes-sidebar]') !== null) { perf.mountAt = performance.now(); return } requestAnimationFrame(check) } requestAnimationFrame(check) })()` function percentile(values: number[], p: number): number { if (values.length === 0) return 0 const sorted = [...values].sort((a, b) => a - b) const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1)) return sorted[index]! } interface ResourceStat { name: string transferSize: number decodedBodySize: number duration: number } /** Dismiss whatever onboarding takeover is present (same dance as the other * lanes) so the pointer can reach the sidebar's controls. */ async function dismissOnboarding(page: Page): Promise { try { await expect .poll(() => page.getByRole('button', { name: /^(Continue|Configure later)$/ }).count(), { timeout: 60_000 }) .toBeGreaterThan(0) } catch { console.warn('[e2e-perf] no onboarding takeover appeared; proceeding') } for (let round = 0; round < 8; round++) { let dismissed = false for (const name of ['Continue', 'Configure later']) { const button = page.getByRole('button', { name, exact: true }).first() if ((await button.count()) === 0) continue try { await button.click({ timeout: 4_000 }) dismissed = true await page.waitForTimeout(1_000) } catch { // Masked by a takeover stacked above; retry in the next round. } } if (!dismissed) break } } test('measure: mount latency, longtasks and bundle cost through a full tab sweep', async ({ page }) => { await page.addInitScript(PERF_PROBE) await page.goto(PAGE_URL, { waitUntil: 'domcontentloaded' }) await expect(page.locator('#root > *')).not.toHaveCount(0, { timeout: 90_000 }) const sidebar = page.locator('[data-dsh-apes-sidebar]') await expect(sidebar).toBeAttached({ timeout: 90_000 }) const mountLatency = await page.evaluate(() => (window as unknown as { __perfProbe: { mountAt: number | null } }).__perfProbe.mountAt) expect(mountLatency, 'the mount probe must have caught the attach').not.toBeNull() await dismissOnboarding(page) // The native Sidebar's way in lives in the conversation header's corner, // which DSH renders only for a session with content. await sendFirstMessage(page) // Open DSH's native right Sidebar and sweep every plugin tab type through // its guide page — the same crash-sweep surface as mount.e2e.ts, reused // here as the workload (the plugin's content lives in the native surface on // 0.1.5; the plugin's own panel keeps only its bottom workbench). await page.locator('[data-sidebar-right-expand]').first().click() const pane = page.locator('[data-sidebar-right-panel]') await expect(pane).toBeVisible({ timeout: 90_000 }) const addTab = page.locator('[data-dockkit-add-tab]').first() for (const kind of NATIVE_TABS) { if (await page.locator('[data-sidebar-right-guide]').count() === 0) await addTab.click() const entry = page.locator(`[data-sidebar-right-guide-entry="${kind}"]`) await expect(entry, `native tab type "${kind}" must be offered by the guide`).toHaveCount(1) await entry.click() await page.waitForTimeout(1_500) } // Let the last tab settle (lazy chunks + first render) before reading. await page.waitForTimeout(2_000) const probe = await page.evaluate(() => (window as unknown as { __perfProbe: { mountAt: number | null; longtasks: Array<{ start: number; duration: number }> } }).__perfProbe) const resources = await page.evaluate(() => performance.getEntriesByType('resource') .filter(entry => /sidebar/.test(entry.name)) .map(entry => { const resource = entry as PerformanceResourceTiming return { name: entry.name.replace(/^https?:\/\/[^/]+/, ''), transferSize: resource.transferSize, decodedBodySize: resource.decodedBodySize, duration: entry.duration, } }) as ResourceStat[], ) const longtasks = probe.longtasks ?? [] const record = { metric: 'mount-sweep', mountLatencyMs: mountLatency, longtaskCount: longtasks.length, longtaskTotalMs: Math.round(longtasks.reduce((sum, task) => sum + task.duration, 0)), longtaskMaxMs: longtasks.reduce((max, task) => Math.max(max, task.duration), 0), resourceCount: resources.length, resourceTransferTotal: resources.reduce((sum, r) => sum + r.transferSize, 0), resourceDecodedTotal: resources.reduce((sum, r) => sum + r.decodedBodySize, 0), resources, } console.log(`PERF_JSON ${JSON.stringify(record)}`) console.log( `PERF_SUMMARY mount=${Math.round(mountLatency!)}ms longtasks=${record.longtaskCount}/${record.longtaskTotalMs}ms ` + `resources=${record.resourceCount}/${(record.resourceTransferTotal / 1024).toFixed(0)}KiB`, ) }) test('measure: bottom-strip drag frame pacing', async ({ page }) => { await page.addInitScript(PERF_PROBE) await page.goto(PAGE_URL, { waitUntil: 'domcontentloaded' }) await expect(page.locator('#root > *')).not.toHaveCount(0, { timeout: 90_000 }) const sidebar = page.locator('[data-dsh-apes-sidebar]') await expect(sidebar).toBeAttached({ timeout: 90_000 }) await dismissOnboarding(page) // The workbench's expand/collapse control is registered into DSH's // session-header utilities, which DSH renders only for a session with // content — seed one message first, then address the toggle by its stable // data attribute (it is NOT inside the plugin's own host). await sendFirstMessage(page) // The plugin pushes no width ever (the right column is DSH's native // Sidebar), so the drag must never write --dsh-sidebar-width — the guard // the drag lane locks, recorded here for the perf story. const bottomExpand = page.locator('[data-dsh-bottom-toggle]') await expect(bottomExpand).toHaveCount(1) await bottomExpand.click() await expect .poll(async () => { const value = await page.evaluate(() => parseFloat(document.documentElement.style.getPropertyValue('--dsh-sidebar-height')) || 0) return value }, { timeout: 90_000 }) .toBeGreaterThan(0) // Frame-pacing sampler: rAF timestamps during the drag only. await page.evaluate(() => { const frames: number[] = [] const pushWidths: number[] = [] const loop = (): void => { frames.push(performance.now()) const width = parseFloat(document.documentElement.style.getPropertyValue('--dsh-sidebar-width')) pushWidths.push(Number.isNaN(width) ? 0 : width) requestAnimationFrame(loop) } requestAnimationFrame(loop) ;(window as unknown as { __dragFrames: number[]; __dragPushWidths: number[] }).__dragFrames = frames ;(window as unknown as { __dragPushWidths: number[] }).__dragPushWidths = pushWidths }) // The panel slides down into place on expand: read the strip only after it // settles at innerHeight - the pushed height (a mid-animation box makes the // pointerdown land off-strip and the drag never starts). await expect .poll(async () => { const probe = await page.evaluate(() => { const bottom = document.querySelector('[data-dsh-apes-sidebar] [data-dsh-bottom-panel]') if (bottom === null) return null const strip = [...bottom.querySelectorAll('*')].find(el => getComputedStyle(el).cursor === 'row-resize') if (strip === undefined) return null const r = strip.getBoundingClientRect() const heightVar = parseFloat(document.documentElement.style.getPropertyValue('--dsh-sidebar-height')) || 0 return Math.abs((r.y + r.height / 2) - (window.innerHeight - heightVar)) }) return probe === null ? Number.NaN : probe }, { timeout: 30_000 }) .toBeLessThanOrEqual(8) const stripPoint = await page.evaluate(() => { const bottom = document.querySelector('[data-dsh-apes-sidebar] [data-dsh-bottom-panel]') if (bottom === null) return null const strip = [...bottom.querySelectorAll('*')].find(el => getComputedStyle(el).cursor === 'row-resize') if (strip === undefined) return null const r = strip.getBoundingClientRect() return { x: r.x + r.width / 2, y: r.y + r.height / 2 } }) expect(stripPoint, 'the bottom drag strip must be present').not.toBeNull() const t0 = await page.evaluate(() => performance.now()) await page.mouse.move(stripPoint!.x, stripPoint!.y) await page.mouse.down() for (let i = 1; i <= 12; i++) { await page.mouse.move(stripPoint!.x, stripPoint!.y - i * 8, { steps: 2 }) await page.waitForTimeout(40) } await page.mouse.up() const t1 = await page.evaluate(() => performance.now()) await page.waitForTimeout(300) const { frames, widths } = await page.evaluate(() => ({ frames: (window as unknown as { __dragFrames: number[] }).__dragFrames, widths: (window as unknown as { __dragPushWidths: number[] }).__dragPushWidths, })) const dragFrames = frames.filter(t => t >= t0 && t <= t1) const intervals: number[] = [] for (let i = 1; i < dragFrames.length; i++) intervals.push(dragFrames[i]! - dragFrames[i - 1]!) const dragWidths = widths.slice(Math.max(0, frames.indexOf(dragFrames[0] ?? frames[0]!))) const record = { metric: 'bottom-drag', frames: dragFrames.length, intervalMedianMs: Math.round(percentile(intervals, 50)), intervalP95Ms: Math.round(percentile(intervals, 95)), intervalMaxMs: Math.round(Math.max(0, ...intervals)), pushWidthLeakMax: Math.max(0, ...dragWidths), } console.log(`PERF_JSON ${JSON.stringify(record)}`) console.log( `PERF_SUMMARY drag-frames=${record.frames} p50=${record.intervalMedianMs}ms p95=${record.intervalP95Ms}ms ` + `max=${record.intervalMaxMs}ms widthLeak=${record.pushWidthLeakMax}px`, ) expect(record.pushWidthLeakMax, 'the plugin must never push a width during the bottom drag').toBe(0) })