mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Add desktop GUI latency harness
Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
co-authored by
Tyler Longwell
parent
cee2c5f263
commit
83cadf0d43
@@ -432,6 +432,55 @@ jobs:
|
||||
fi
|
||||
echo "Desktop E2E Integration shards passed"
|
||||
|
||||
desktop-e2e-perf:
|
||||
name: Desktop GUI Latency Harness
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
continue-on-error: true
|
||||
needs: [changes]
|
||||
if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||
- name: Restore pnpm store cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: pnpm-${{ runner.os }}-
|
||||
- name: Install desktop dependencies
|
||||
run: just desktop-install-ci
|
||||
- name: Get Playwright version
|
||||
id: pw-version
|
||||
run: echo "version=$(cd desktop && node -e "console.log(require('@playwright/test/package.json').version)")" >> "$GITHUB_OUTPUT"
|
||||
- name: Restore Playwright browser cache
|
||||
id: playwright-cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
|
||||
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
|
||||
- name: Install Playwright Chromium
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: cd desktop && pnpm exec playwright install chromium
|
||||
- name: Install Playwright system dependencies
|
||||
run: cd desktop && pnpm exec playwright install-deps chromium
|
||||
- name: Desktop GUI latency harness
|
||||
run: just desktop-e2e-perf
|
||||
- name: Upload desktop latency artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: desktop-gui-latency-artifacts
|
||||
path: |
|
||||
desktop/playwright-report
|
||||
desktop/test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
backend-integration:
|
||||
name: Backend Integration (relay e2e)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -216,6 +216,10 @@ desktop-e2e-smoke:
|
||||
desktop-e2e-integration: _ensure-migrations
|
||||
cd {{desktop_dir}} && pnpm test:e2e:integration
|
||||
|
||||
# Run desktop read-only GUI latency harness against the mock bridge
|
||||
desktop-e2e-perf:
|
||||
cd {{desktop_dir}} && pnpm test:e2e:perf
|
||||
|
||||
# Run all checks suitable for CI / pre-push (no infra needed)
|
||||
ci: check test-unit desktop-test desktop-build desktop-tauri-check desktop-tauri-test web-build mobile-test
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"test:e2e": "pnpm build && playwright test",
|
||||
"test:e2e:smoke": "pnpm build && playwright test --project=smoke",
|
||||
"test:e2e:integration": "pnpm build && playwright test --project=integration",
|
||||
"test:e2e:perf": "pnpm build && playwright test --config=playwright.perf.config.ts",
|
||||
"test:e2e:report": "playwright show-report",
|
||||
"tauri:build": "tauri build"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
import { TEST_IDENTITIES, installMockBridge } from "../helpers/bridge";
|
||||
import { logMeasurement, measureAction } from "./perf/metrics";
|
||||
|
||||
const BUSY_ROWS = 220;
|
||||
const THREAD_REPLIES = 24;
|
||||
const TYPING_SAMPLE =
|
||||
"Drafting a latency repro with @alice, a second paragraph, and enough text to exercise wrapping in the composer.";
|
||||
|
||||
type MockMessageEvent = { id: string; created_at: number; pubkey: string };
|
||||
|
||||
async function waitForMockHooks(page: Page) {
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
typeof window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function" &&
|
||||
typeof window.__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__ === "function",
|
||||
);
|
||||
}
|
||||
|
||||
async function emitMockMessage(
|
||||
page: Page,
|
||||
channelName: string,
|
||||
content: string,
|
||||
options?: {
|
||||
createdAt?: number;
|
||||
parentEventId?: string;
|
||||
pubkey?: string;
|
||||
},
|
||||
): Promise<MockMessageEvent> {
|
||||
const event = await page.evaluate(
|
||||
({ channelName: ch, content: body, createdAt, parentEventId, pubkey }) =>
|
||||
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
||||
channelName: ch,
|
||||
content: body,
|
||||
createdAt,
|
||||
parentEventId,
|
||||
pubkey,
|
||||
}),
|
||||
{
|
||||
channelName,
|
||||
content,
|
||||
createdAt: options?.createdAt,
|
||||
parentEventId: options?.parentEventId,
|
||||
pubkey: options?.pubkey ?? TEST_IDENTITIES.alice.pubkey,
|
||||
},
|
||||
);
|
||||
if (!event) throw new Error("Mock message emitter is not installed");
|
||||
return event;
|
||||
}
|
||||
|
||||
async function seedBusyChannel(
|
||||
page: Page,
|
||||
channelName: string,
|
||||
rows = BUSY_ROWS,
|
||||
) {
|
||||
await page.evaluate(
|
||||
({ channelName: ch, rows }) => {
|
||||
for (let i = 0; i < rows; i += 1) {
|
||||
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
||||
channelName: ch,
|
||||
content: `latency seed ${ch} row ${i}\nsecond line to exercise wrapping`,
|
||||
});
|
||||
}
|
||||
},
|
||||
{ channelName, rows },
|
||||
);
|
||||
}
|
||||
|
||||
async function clickChannel(page: Page, channelName: string) {
|
||||
const channel = page.getByTestId(`channel-${channelName}`);
|
||||
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
||||
await expect(channel).toBeVisible();
|
||||
|
||||
// The sidebar can briefly re-render while mock live messages update unread
|
||||
// state. Keep setup resilient by retrying the locator, which re-resolves on
|
||||
// each attempt instead of holding a stale DOM node.
|
||||
let lastError: unknown;
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
await channel.click({ timeout: 5_000 });
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
await page.waitForTimeout(100);
|
||||
await expect(channel).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function openChannel(page: Page, channelName: string) {
|
||||
await clickChannel(page, channelName);
|
||||
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
||||
await expect(page.getByTestId("message-row").first()).toBeVisible();
|
||||
}
|
||||
|
||||
async function mountedRowCount(page: Page) {
|
||||
return page.getByTestId("message-row").count();
|
||||
}
|
||||
|
||||
test.describe("Buzz GUI latency harness", () => {
|
||||
test("MEASURE: composer typing latency in a busy channel", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
await waitForMockHooks(page);
|
||||
await seedBusyChannel(page, "general");
|
||||
await openChannel(page, "general");
|
||||
|
||||
const input = page.getByTestId("message-input");
|
||||
await expect(input).toBeVisible();
|
||||
|
||||
const measurement = await measureAction(page, async () => {
|
||||
await input.click();
|
||||
await page.keyboard.type(TYPING_SAMPLE, { delay: 0 });
|
||||
await expect(input).toContainText("latency repro");
|
||||
return {
|
||||
chars: TYPING_SAMPLE.length,
|
||||
rows: await mountedRowCount(page),
|
||||
};
|
||||
});
|
||||
|
||||
logMeasurement("COMPOSER TYPING LATENCY (busy channel, no send)", {
|
||||
"chars typed": measurement.result.chars,
|
||||
"rows mounted": measurement.result.rows,
|
||||
"wall time": `${measurement.wallMs.toFixed(1)}ms`,
|
||||
"ms / char": (measurement.wallMs / measurement.result.chars).toFixed(2),
|
||||
"layout time": `${measurement.metrics.layoutMs.toFixed(1)}ms`,
|
||||
"style recalc": `${measurement.metrics.recalcMs.toFixed(1)}ms`,
|
||||
"script time": `${measurement.metrics.scriptMs.toFixed(1)}ms`,
|
||||
"task time": `${measurement.metrics.taskMs.toFixed(1)}ms`,
|
||||
"layout count": measurement.metrics.layoutCount,
|
||||
});
|
||||
|
||||
expect(measurement.result.chars).toBeGreaterThan(80);
|
||||
expect(measurement.result.rows).toBeGreaterThan(50);
|
||||
expect(measurement.wallMs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("MEASURE: channel switch latency across seeded busy channels", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, { historyDelayMs: 120 });
|
||||
await page.goto("/");
|
||||
await waitForMockHooks(page);
|
||||
await seedBusyChannel(page, "general", 160);
|
||||
await seedBusyChannel(page, "engineering", 160);
|
||||
await openChannel(page, "general");
|
||||
|
||||
const measurement = await measureAction(page, async () => {
|
||||
await clickChannel(page, "engineering");
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
|
||||
await expect(page.getByTestId("message-row").first()).toBeVisible();
|
||||
return { rows: await mountedRowCount(page) };
|
||||
});
|
||||
|
||||
logMeasurement("CHANNEL SWITCH LATENCY (general → engineering)", {
|
||||
"rows mounted": measurement.result.rows,
|
||||
"wall time": `${measurement.wallMs.toFixed(1)}ms`,
|
||||
"layout time": `${measurement.metrics.layoutMs.toFixed(1)}ms`,
|
||||
"style recalc": `${measurement.metrics.recalcMs.toFixed(1)}ms`,
|
||||
"script time": `${measurement.metrics.scriptMs.toFixed(1)}ms`,
|
||||
"task time": `${measurement.metrics.taskMs.toFixed(1)}ms`,
|
||||
"layout count": measurement.metrics.layoutCount,
|
||||
});
|
||||
|
||||
expect(measurement.result.rows).toBeGreaterThan(20);
|
||||
expect(measurement.wallMs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("MEASURE: thread panel open latency with seeded replies", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
await waitForMockHooks(page);
|
||||
await openChannel(page, "general");
|
||||
|
||||
for (let i = 0; i < THREAD_REPLIES; i += 1) {
|
||||
await emitMockMessage(page, "general", `thread latency reply ${i + 1}`, {
|
||||
parentEventId: "mock-general-welcome",
|
||||
pubkey: TEST_IDENTITIES.alice.pubkey,
|
||||
});
|
||||
}
|
||||
|
||||
const threadSummary = page.getByTestId("message-thread-summary").first();
|
||||
await expect(threadSummary).toBeVisible();
|
||||
|
||||
const measurement = await measureAction(page, async () => {
|
||||
await threadSummary.click();
|
||||
const panel = page.getByTestId("message-thread-panel");
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(
|
||||
panel.getByText("thread latency reply 1", { exact: true }),
|
||||
).toBeVisible();
|
||||
return {
|
||||
replies: await panel.getByTestId("message-row").count(),
|
||||
};
|
||||
});
|
||||
|
||||
logMeasurement("THREAD OPEN LATENCY (seeded replies)", {
|
||||
"replies rendered": measurement.result.replies,
|
||||
"wall time": `${measurement.wallMs.toFixed(1)}ms`,
|
||||
"layout time": `${measurement.metrics.layoutMs.toFixed(1)}ms`,
|
||||
"style recalc": `${measurement.metrics.recalcMs.toFixed(1)}ms`,
|
||||
"script time": `${measurement.metrics.scriptMs.toFixed(1)}ms`,
|
||||
"task time": `${measurement.metrics.taskMs.toFixed(1)}ms`,
|
||||
"layout count": measurement.metrics.layoutCount,
|
||||
});
|
||||
|
||||
expect(measurement.result.replies).toBeGreaterThan(5);
|
||||
expect(measurement.wallMs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("MEASURE: member search latency in add-people sidebar", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, { userSearchDelayMs: 180 });
|
||||
await page.goto("/");
|
||||
await waitForMockHooks(page);
|
||||
await openChannel(page, "general");
|
||||
await page.getByTestId("channel-members-trigger").click();
|
||||
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
||||
|
||||
const search = page.getByTestId("channel-management-search-users");
|
||||
await expect(search).toBeVisible();
|
||||
|
||||
const measurement = await measureAction(page, async () => {
|
||||
await search.fill("outsider");
|
||||
const result = page
|
||||
.locator('[data-testid^="channel-user-search-result-"]')
|
||||
.first();
|
||||
await expect(result).toBeVisible();
|
||||
return {
|
||||
resultText: ((await result.textContent()) ?? "").trim(),
|
||||
};
|
||||
});
|
||||
|
||||
logMeasurement("ADD-PEOPLE SEARCH LATENCY (read-only)", {
|
||||
"first result": measurement.result.resultText,
|
||||
"wall time": `${measurement.wallMs.toFixed(1)}ms`,
|
||||
"layout time": `${measurement.metrics.layoutMs.toFixed(1)}ms`,
|
||||
"style recalc": `${measurement.metrics.recalcMs.toFixed(1)}ms`,
|
||||
"script time": `${measurement.metrics.scriptMs.toFixed(1)}ms`,
|
||||
"task time": `${measurement.metrics.taskMs.toFixed(1)}ms`,
|
||||
"layout count": measurement.metrics.layoutCount,
|
||||
});
|
||||
|
||||
expect(measurement.result.resultText.toLowerCase()).toContain("outsider");
|
||||
expect(measurement.wallMs).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { CDPSession, Page } from "@playwright/test";
|
||||
|
||||
export type BrowserMetrics = {
|
||||
layoutMs: number;
|
||||
recalcMs: number;
|
||||
layoutCount: number;
|
||||
scriptMs: number;
|
||||
taskMs: number;
|
||||
};
|
||||
|
||||
export type ActionMeasurement<T> = {
|
||||
metrics: BrowserMetrics;
|
||||
result: T;
|
||||
wallMs: number;
|
||||
};
|
||||
|
||||
export async function readBrowserMetrics(
|
||||
client: CDPSession,
|
||||
): Promise<BrowserMetrics> {
|
||||
const { metrics } = (await client.send("Performance.getMetrics")) as {
|
||||
metrics: Array<{ name: string; value: number }>;
|
||||
};
|
||||
const m = (name: string) => metrics.find((x) => x.name === name)?.value ?? 0;
|
||||
return {
|
||||
// CDP reports durations in seconds; convert to ms.
|
||||
layoutMs: m("LayoutDuration") * 1000,
|
||||
recalcMs: m("RecalcStyleDuration") * 1000,
|
||||
layoutCount: m("LayoutCount"),
|
||||
scriptMs: m("ScriptDuration") * 1000,
|
||||
taskMs: m("TaskDuration") * 1000,
|
||||
};
|
||||
}
|
||||
|
||||
function deltaMetrics(after: BrowserMetrics, before: BrowserMetrics) {
|
||||
return {
|
||||
layoutMs: after.layoutMs - before.layoutMs,
|
||||
recalcMs: after.recalcMs - before.recalcMs,
|
||||
layoutCount: after.layoutCount - before.layoutCount,
|
||||
scriptMs: after.scriptMs - before.scriptMs,
|
||||
taskMs: after.taskMs - before.taskMs,
|
||||
} satisfies BrowserMetrics;
|
||||
}
|
||||
|
||||
export async function measureAction<T>(
|
||||
page: Page,
|
||||
action: () => Promise<T>,
|
||||
): Promise<ActionMeasurement<T>> {
|
||||
const client = await page.context().newCDPSession(page);
|
||||
await client.send("Performance.enable");
|
||||
const before = await readBrowserMetrics(client);
|
||||
const start = performance.now();
|
||||
const result = await action();
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise<void>((resolve) => requestAnimationFrame(() => resolve())),
|
||||
);
|
||||
const wallMs = performance.now() - start;
|
||||
const after = await readBrowserMetrics(client);
|
||||
await client.send("Performance.disable");
|
||||
return { metrics: deltaMetrics(after, before), result, wallMs };
|
||||
}
|
||||
|
||||
export function logMeasurement(
|
||||
title: string,
|
||||
fields: Record<string, string | number>,
|
||||
) {
|
||||
const width = Math.max(...Object.keys(fields).map((key) => key.length));
|
||||
/* eslint-disable no-console */
|
||||
console.log(`\n=== ${title} ===`);
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
console.log(`${key.padEnd(width)}: ${value}`);
|
||||
}
|
||||
console.log("========================================\n");
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
import { logMeasurement, readBrowserMetrics } from "./perf/metrics";
|
||||
|
||||
/**
|
||||
* Scroll-smoothness measurement harness.
|
||||
@@ -38,23 +39,6 @@ type Sample = {
|
||||
frames: number;
|
||||
};
|
||||
|
||||
type Metrics = { layoutMs: number; recalcMs: number; layoutCount: number };
|
||||
|
||||
async function readMetrics(
|
||||
client: import("@playwright/test").CDPSession,
|
||||
): Promise<Metrics> {
|
||||
const { metrics } = (await client.send("Performance.getMetrics")) as {
|
||||
metrics: Array<{ name: string; value: number }>;
|
||||
};
|
||||
const m = (name: string) => metrics.find((x) => x.name === name)?.value ?? 0;
|
||||
return {
|
||||
// CDP reports durations in seconds; convert to ms.
|
||||
layoutMs: m("LayoutDuration") * 1000,
|
||||
recalcMs: m("RecalcStyleDuration") * 1000,
|
||||
layoutCount: m("LayoutCount"),
|
||||
};
|
||||
}
|
||||
|
||||
test("MEASURE: fast-wheel scroll-up layout cost on a busy un-virtualized timeline", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -111,7 +95,7 @@ test("MEASURE: fast-wheel scroll-up layout cost on a busy un-virtualized timelin
|
||||
});
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
const before = await readMetrics(client);
|
||||
const before = await readBrowserMetrics(client);
|
||||
|
||||
const sample = await timeline.evaluate(async (element): Promise<Sample> => {
|
||||
const el = element as HTMLDivElement;
|
||||
@@ -154,7 +138,7 @@ test("MEASURE: fast-wheel scroll-up layout cost on a busy un-virtualized timelin
|
||||
};
|
||||
});
|
||||
|
||||
const after = await readMetrics(client);
|
||||
const after = await readBrowserMetrics(client);
|
||||
await client.send("Performance.disable");
|
||||
|
||||
const layoutDelta = after.layoutMs - before.layoutMs;
|
||||
@@ -162,22 +146,16 @@ test("MEASURE: fast-wheel scroll-up layout cost on a busy un-virtualized timelin
|
||||
const layoutCountDelta = after.layoutCount - before.layoutCount;
|
||||
const perScroll = sample.frames > 0 ? sample.frames : 1;
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log("\n=== SCROLL SMOOTHNESS BASELINE (Chromium layout cost) ===");
|
||||
console.log(`rows mounted (live DOM): ${sample.rowCount}`);
|
||||
console.log(
|
||||
`scroll span covered: ${Math.round(sample.scrollSpan)}px`,
|
||||
);
|
||||
console.log(`scroll frames driven: ${sample.frames}`);
|
||||
console.log(`layout time over burst: ${layoutDelta.toFixed(1)}ms`);
|
||||
console.log(`style-recalc time over burst: ${recalcDelta.toFixed(1)}ms`);
|
||||
console.log(`forced layouts (count delta): ${layoutCountDelta}`);
|
||||
console.log(
|
||||
`avg layout+recalc per frame: ${((layoutDelta + recalcDelta) / perScroll).toFixed(3)}ms`,
|
||||
);
|
||||
console.log("(>~8ms/frame main-thread work is where 120Hz starts to drop)");
|
||||
console.log("=========================================================\n");
|
||||
/* eslint-enable no-console */
|
||||
logMeasurement("SCROLL SMOOTHNESS BASELINE (Chromium layout cost)", {
|
||||
"rows mounted (live DOM)": sample.rowCount,
|
||||
"scroll span covered": `${Math.round(sample.scrollSpan)}px`,
|
||||
"scroll frames driven": sample.frames,
|
||||
"layout time over burst": `${layoutDelta.toFixed(1)}ms`,
|
||||
"style-recalc time over burst": `${recalcDelta.toFixed(1)}ms`,
|
||||
"forced layouts (count delta)": layoutCountDelta,
|
||||
"avg layout+recalc per frame": `${((layoutDelta + recalcDelta) / perScroll).toFixed(3)}ms`,
|
||||
note: ">~8ms/frame main-thread work is where 120Hz starts to drop",
|
||||
});
|
||||
|
||||
// Instrument, not a gate — just confirm it exercised the full list.
|
||||
expect(sample.rowCount).toBeGreaterThan(100);
|
||||
@@ -257,7 +235,7 @@ test("MEASURE: prepend re-render cost while scrolled up (the untested event-cost
|
||||
const client = await page.context().newCDPSession(page);
|
||||
await client.send("Performance.enable");
|
||||
|
||||
const before = await readMetrics(client);
|
||||
const before = await readBrowserMetrics(client);
|
||||
const scrollTopBefore = await timeline.evaluate(
|
||||
(el) => (el as HTMLDivElement).scrollTop,
|
||||
);
|
||||
@@ -280,7 +258,7 @@ test("MEASURE: prepend re-render cost while scrolled up (the untested event-cost
|
||||
return performance.now() - t0;
|
||||
}, 10);
|
||||
|
||||
const after = await readMetrics(client);
|
||||
const after = await readBrowserMetrics(client);
|
||||
const scrollTopAfter = await timeline.evaluate(
|
||||
(el) => (el as HTMLDivElement).scrollTop,
|
||||
);
|
||||
@@ -293,18 +271,14 @@ test("MEASURE: prepend re-render cost while scrolled up (the untested event-cost
|
||||
const recalcDelta = after.recalcMs - before.recalcMs;
|
||||
const anchorDrift = scrollTopAfter - scrollTopBefore;
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log("\n=== PREPEND RE-RENDER COST (10 older rows, scrolled up) ===");
|
||||
console.log(`rows after prepend (live DOM): ${rowCountAfter}`);
|
||||
console.log(`tick wall-time (commit+layout): ${tickMs.toFixed(2)}ms`);
|
||||
console.log(`layout time attributed: ${layoutDelta.toFixed(1)}ms`);
|
||||
console.log(`style-recalc time attributed: ${recalcDelta.toFixed(1)}ms`);
|
||||
console.log(
|
||||
`anchor drift (scrollTop delta): ${anchorDrift.toFixed(1)}px (0 = held)`,
|
||||
);
|
||||
console.log("(tick > ~8ms during active wheel is where 120Hz stalls)");
|
||||
console.log("===========================================================\n");
|
||||
/* eslint-enable no-console */
|
||||
logMeasurement("PREPEND RE-RENDER COST (10 older rows, scrolled up)", {
|
||||
"rows after prepend (live DOM)": rowCountAfter,
|
||||
"tick wall-time (commit+layout)": `${tickMs.toFixed(2)}ms`,
|
||||
"layout time attributed": `${layoutDelta.toFixed(1)}ms`,
|
||||
"style-recalc time attributed": `${recalcDelta.toFixed(1)}ms`,
|
||||
"anchor drift (scrollTop delta)": `${anchorDrift.toFixed(1)}px (0 = held)`,
|
||||
note: "tick > ~8ms during active wheel is where 120Hz stalls",
|
||||
});
|
||||
|
||||
// Instrument, not a gate — just confirm the prepend actually happened.
|
||||
expect(rowCountAfter).toBeGreaterThan(50);
|
||||
|
||||
Reference in New Issue
Block a user