feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
|
||||
/**
|
||||
* Warm-channel-switch benchmark.
|
||||
*
|
||||
* Measures the felt cost of switching INTO a channel whose messages are
|
||||
* already in the React Query cache (the everyday alt-tab-between-channels
|
||||
* motion). The timeline subtree is keyed by channel id — required so TanStack
|
||||
* Router's scroll restoration never writes a stale scrollTop into a reused
|
||||
* scroll node — so every switch unmounts and remounts all rows, and each
|
||||
* `MessageRow` re-runs the synchronous react-markdown parse pipeline from
|
||||
* scratch. This spec is the instrument for that cost.
|
||||
*
|
||||
* TWO SCENARIOS, one per axis of the cost:
|
||||
* plain-text — `deep-history` (600 seeded one-line rows; the initial
|
||||
* channel window mounts ~50 of them, verified by parse
|
||||
* count): isolates the per-row remount floor.
|
||||
* markdown — `random` + 60 injected markdown-heavy rows (code fences,
|
||||
* tables, lists, mentions, links): isolates the parse cost the
|
||||
* markdown cache is meant to remove.
|
||||
*
|
||||
* WHAT A "SWITCH" MEASURES: performance.now() immediately before an in-page
|
||||
* .click() on the sidebar link, until (chat title flipped) AND (>= 1 message
|
||||
* row committed) AND (no [data-render-pending="true"], i.e. the deferred
|
||||
* timeline snapshot caught up to the live one) AND a double-rAF so a frame
|
||||
* actually painted. The click and the polling both run in-page so CDP
|
||||
* round-trip latency never pollutes the numbers. Longtask totals are captured
|
||||
* per switch as the "UI froze" axis (see cold-switch-longtask.perf.ts for the
|
||||
* rationale).
|
||||
*
|
||||
* WARM means every measured entry is a RE-entry: each scenario does one
|
||||
* untimed round-trip first so both channels' queries are cached and code
|
||||
* paths are jitted. 4x CPU throttle for the same reason as the cold spec —
|
||||
* absolute ms are not portable across machines, but before/after deltas on
|
||||
* the same machine are.
|
||||
*
|
||||
* Run it (from desktop/):
|
||||
* pnpm build
|
||||
* npx playwright test --config=playwright.perf.config.ts warm-switch-markdown.perf.ts
|
||||
*
|
||||
* NOTE: the perf web server reuses an existing server on :4173 — if one is
|
||||
* already running, kill it or make sure `dist/` is freshly built, otherwise
|
||||
* you measure stale code.
|
||||
*/
|
||||
|
||||
const MEASURED_SWITCHES = 8;
|
||||
const THROTTLE_RATE = 4;
|
||||
const MARKDOWN_MESSAGE_COUNT = 60;
|
||||
|
||||
/** One representative agent-style message: fence, table, list, mention,
|
||||
* emphasis, inline code, and a link — the mix real Buzz channels carry. */
|
||||
function markdownBody(index: number): string {
|
||||
return [
|
||||
`**Update ${index}** from the build agent — _step ${index} of ${MARKDOWN_MESSAGE_COUNT}_ :tada:`,
|
||||
"",
|
||||
"```rust",
|
||||
`fn step_${index}() -> Result<Status, Error> {`,
|
||||
' let plan = load_plan("release")?;',
|
||||
` plan.execute(${index})`,
|
||||
"}",
|
||||
"```",
|
||||
"",
|
||||
"| check | result | took |",
|
||||
"|-------|--------|------|",
|
||||
`| clippy | ok | ${index}ms |`,
|
||||
`| fmt | ok | ${index + 1}ms |`,
|
||||
"",
|
||||
`- [x] compile stage ${index}`,
|
||||
"- [ ] publish artifacts",
|
||||
`- see [pipeline](https://example.com/build/${index}) or ask @alice`,
|
||||
"",
|
||||
`Inline \`cargo build -p step-${index}\` finished.`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
type SwitchSample = {
|
||||
ms: number;
|
||||
longtaskTotal: number;
|
||||
longtaskMax: number;
|
||||
longtaskCount: number;
|
||||
};
|
||||
|
||||
function median(values: number[]): number {
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const mid = Math.floor(sorted.length / 2);
|
||||
return sorted.length % 2 === 0
|
||||
? (sorted[mid - 1] + sorted[mid]) / 2
|
||||
: sorted[mid];
|
||||
}
|
||||
|
||||
async function waitForMockLiveSubscription(
|
||||
page: import("@playwright/test").Page,
|
||||
channelName: string,
|
||||
) {
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
(ch) =>
|
||||
window.__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({
|
||||
channelName: ch,
|
||||
}) ?? false,
|
||||
channelName,
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
/** Click the sidebar link and poll — all in-page — until the target channel's
|
||||
* rows are committed, the deferred snapshot has caught up, and a frame
|
||||
* painted. Returns wall-clock ms plus the longtasks observed in the window. */
|
||||
async function measureSwitch(
|
||||
page: import("@playwright/test").Page,
|
||||
input: { targetTestId: string; targetTitle: string; rowSelector: string },
|
||||
): Promise<SwitchSample> {
|
||||
return page.evaluate(async (args) => {
|
||||
const store = window as unknown as { __LONGTASKS__: number[] };
|
||||
store.__LONGTASKS__ = [];
|
||||
const link = document.querySelector<HTMLElement>(
|
||||
`[data-testid="${args.targetTestId}"]`,
|
||||
);
|
||||
if (!link) throw new Error(`missing sidebar link ${args.targetTestId}`);
|
||||
|
||||
const start = performance.now();
|
||||
link.click();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const deadline = start + 30_000;
|
||||
const check = () => {
|
||||
const title = document.querySelector(
|
||||
'[data-testid="chat-title"]',
|
||||
)?.textContent;
|
||||
const ready =
|
||||
title === args.targetTitle &&
|
||||
document.querySelector(args.rowSelector) !== null &&
|
||||
document.querySelector('[data-render-pending="true"]') === null;
|
||||
if (ready) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
||||
return;
|
||||
}
|
||||
if (performance.now() > deadline) {
|
||||
reject(new Error(`switch to ${args.targetTitle} timed out`));
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(check);
|
||||
};
|
||||
requestAnimationFrame(check);
|
||||
});
|
||||
|
||||
const elapsed = performance.now() - start;
|
||||
const tasks = store.__LONGTASKS__ ?? [];
|
||||
return {
|
||||
ms: elapsed,
|
||||
longtaskTotal: tasks.reduce((sum, duration) => sum + duration, 0),
|
||||
longtaskMax: tasks.length ? Math.max(...tasks) : 0,
|
||||
longtaskCount: tasks.length,
|
||||
};
|
||||
}, input);
|
||||
}
|
||||
|
||||
async function runScenario(
|
||||
page: import("@playwright/test").Page,
|
||||
input: {
|
||||
label: string;
|
||||
targetTestId: string;
|
||||
targetTitle: string;
|
||||
rowSelector: string;
|
||||
},
|
||||
): Promise<SwitchSample[]> {
|
||||
const back = {
|
||||
targetTestId: "channel-general",
|
||||
targetTitle: "general",
|
||||
rowSelector: "[data-message-id]",
|
||||
};
|
||||
|
||||
// Untimed warmup round-trip: caches both channels' queries.
|
||||
await measureSwitch(page, input);
|
||||
await measureSwitch(page, back);
|
||||
|
||||
const samples: SwitchSample[] = [];
|
||||
for (let run = 0; run < MEASURED_SWITCHES; run += 1) {
|
||||
samples.push(await measureSwitch(page, input));
|
||||
await measureSwitch(page, back);
|
||||
}
|
||||
|
||||
const times = samples.map((sample) => sample.ms);
|
||||
const longtaskTotals = samples.map((sample) => sample.longtaskTotal);
|
||||
/* eslint-disable no-console */
|
||||
console.log(`\n=== WARM SWITCH: ${input.label} ===`);
|
||||
console.log(`CPU throttle: ${THROTTLE_RATE}x`);
|
||||
console.log(
|
||||
`per-switch wall ms: [${times.map((v) => v.toFixed(1)).join(", ")}]`,
|
||||
);
|
||||
console.log(
|
||||
`per-switch longtask ms: [${longtaskTotals.map((v) => v.toFixed(1)).join(", ")}]`,
|
||||
);
|
||||
console.log(`MEDIAN wall ms: ${median(times).toFixed(1)}`);
|
||||
console.log(
|
||||
`MEDIAN longtask total: ${median(longtaskTotals).toFixed(1)}ms`,
|
||||
);
|
||||
console.log(
|
||||
`worst single longtask: ${Math.max(...samples.map((sample) => sample.longtaskMax)).toFixed(1)}ms`,
|
||||
);
|
||||
/* eslint-enable no-console */
|
||||
return samples;
|
||||
}
|
||||
|
||||
test("MEASURE: warm channel-switch cost (plain 300-row + markdown-heavy)", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(300_000);
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
await page.waitForFunction(
|
||||
() => typeof window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function",
|
||||
);
|
||||
|
||||
// Arm the longtask observer; addInitScript applies on next navigation.
|
||||
await page.addInitScript(() => {
|
||||
const store = window as unknown as { __LONGTASKS__?: number[] };
|
||||
store.__LONGTASKS__ = [];
|
||||
new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
store.__LONGTASKS__?.push(entry.duration);
|
||||
}
|
||||
}).observe({ type: "longtask", buffered: true });
|
||||
});
|
||||
await page.reload();
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
typeof window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function" &&
|
||||
Array.isArray(
|
||||
(window as unknown as { __LONGTASKS__?: number[] }).__LONGTASKS__,
|
||||
),
|
||||
);
|
||||
|
||||
// Seed `random` with markdown-heavy rows. Live emits need an active
|
||||
// subscription, so enter the channel first; the mock store keeps the rows
|
||||
// for every later re-entry via get_channel_window.
|
||||
await page.getByTestId("channel-random").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("random");
|
||||
await waitForMockLiveSubscription(page, "random");
|
||||
await page.evaluate(
|
||||
({ count, bodies }) => {
|
||||
const base = Math.floor(Date.now() / 1000) - count - 10;
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
||||
channelName: "random",
|
||||
content: bodies[index],
|
||||
createdAt: base + index,
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
count: MARKDOWN_MESSAGE_COUNT,
|
||||
bodies: Array.from({ length: MARKDOWN_MESSAGE_COUNT }, (_, index) =>
|
||||
markdownBody(index),
|
||||
),
|
||||
},
|
||||
);
|
||||
// All injected rows committed before anything is timed.
|
||||
await expect(
|
||||
page.locator(`text=Update ${MARKDOWN_MESSAGE_COUNT - 1}`).first(),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const client = await page.context().newCDPSession(page);
|
||||
await client.send("Emulation.setCPUThrottlingRate", { rate: THROTTLE_RATE });
|
||||
|
||||
const plain = await runScenario(page, {
|
||||
label: "plain-text ~50-row window (deep-history)",
|
||||
targetTestId: "channel-deep-history",
|
||||
targetTitle: "deep-history",
|
||||
rowSelector: '[data-message-id^="mock-deep-history-"]',
|
||||
});
|
||||
const markdown = await runScenario(page, {
|
||||
label: `markdown-heavy x${MARKDOWN_MESSAGE_COUNT} (random)`,
|
||||
targetTestId: "channel-random",
|
||||
targetTitle: "random",
|
||||
rowSelector: "[data-message-id]",
|
||||
});
|
||||
|
||||
await client.send("Emulation.setCPUThrottlingRate", { rate: 1 });
|
||||
|
||||
// Instrument, not a gate: assert the harness measured real work.
|
||||
expect(plain.length).toBe(MEASURED_SWITCHES);
|
||||
expect(markdown.length).toBe(MEASURED_SWITCHES);
|
||||
expect(plain.every((sample) => sample.ms > 0)).toBe(true);
|
||||
expect(markdown.every((sample) => sample.ms > 0)).toBe(true);
|
||||
});
|
||||
Reference in New Issue
Block a user