9dfa06ffee
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>
587 lines
20 KiB
TypeScript
587 lines
20 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { installMockBridge } from "../helpers/bridge";
|
|
|
|
/**
|
|
* Composer typing-latency benchmark.
|
|
*
|
|
* Measures per-keystroke input-to-paint latency in the message composer,
|
|
* quiet vs. under simulated agent activity (typing indicators from N agent
|
|
* pubkeys + periodic live messages), to quantify the "typing feels slow in
|
|
* agent-busy channels" report.
|
|
*
|
|
* METRIC: the browser's Event Timing API (`PerformanceObserver` type
|
|
* "event") — each entry's `duration` is input timestamp → next paint, the
|
|
* engine-level definition of keystroke responsiveness (8ms granularity).
|
|
* We record entries for `input` events with durationThreshold 16ms and
|
|
* report median/p95/max plus the count of >50ms (frame-budget-blowing)
|
|
* keystrokes. Longtask totals are captured per scenario as a second axis.
|
|
*
|
|
* SCENARIOS (same 80-char typing burst, 4x CPU throttle):
|
|
* quiet — no agent traffic.
|
|
* busy — 8 agent pubkeys emit typing indicators round-robin every 250ms
|
|
* (≈ the arrival rate of 8 agents refreshing every 2-3s), plus a
|
|
* live markdown message lands every 2s.
|
|
*
|
|
* Absolute ms are machine-specific; the quiet-vs-busy DELTA on one machine
|
|
* is the signal. Run it (from desktop/):
|
|
* pnpm build
|
|
* npx playwright test --config=playwright.perf.config.ts typing-latency.perf.ts
|
|
*/
|
|
|
|
const THROTTLE_RATE = 4;
|
|
const TYPED_TEXT =
|
|
"The quick brown fox jumps over the lazy dog while agents keep working away";
|
|
const KEY_DELAY_MS = 60;
|
|
const AGENT_COUNT = 8;
|
|
const TYPING_EMIT_INTERVAL_MS = 250;
|
|
const LIVE_MESSAGE_INTERVAL_MS = 2000;
|
|
|
|
type LatencyReport = {
|
|
count: number;
|
|
median: number;
|
|
p95: number;
|
|
max: number;
|
|
over50: number;
|
|
longtaskTotal: number;
|
|
};
|
|
|
|
async function resetWindowMetrics(page: import("@playwright/test").Page) {
|
|
await page.evaluate(() => {
|
|
const store = window as unknown as {
|
|
__INPUT_EVENTS__: number[];
|
|
__LONGTASKS__: number[];
|
|
};
|
|
store.__INPUT_EVENTS__ = [];
|
|
store.__LONGTASKS__ = [];
|
|
});
|
|
}
|
|
|
|
async function readWindowMetrics(
|
|
page: import("@playwright/test").Page,
|
|
): Promise<LatencyReport> {
|
|
return page.evaluate(() => {
|
|
const store = window as unknown as {
|
|
__INPUT_EVENTS__: number[];
|
|
__LONGTASKS__: number[];
|
|
};
|
|
const durations = [...(store.__INPUT_EVENTS__ ?? [])].sort((a, b) => a - b);
|
|
const at = (q: number) =>
|
|
durations.length === 0
|
|
? 0
|
|
: durations[
|
|
Math.min(
|
|
durations.length - 1,
|
|
Math.floor(q * (durations.length - 1)),
|
|
)
|
|
];
|
|
return {
|
|
count: durations.length,
|
|
median: at(0.5),
|
|
p95: at(0.95),
|
|
max: durations.length ? durations[durations.length - 1] : 0,
|
|
over50: durations.filter((d) => d > 50).length,
|
|
longtaskTotal: (store.__LONGTASKS__ ?? []).reduce((s, d) => s + d, 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
async function typeBurst(
|
|
page: import("@playwright/test").Page,
|
|
scope?: import("@playwright/test").Locator,
|
|
) {
|
|
const input = (scope ?? page).getByTestId("message-input").last();
|
|
await input.click();
|
|
await input.pressSequentially(TYPED_TEXT, { delay: KEY_DELAY_MS });
|
|
// Let trailing event-timing entries (reported after paint) flush.
|
|
await page.waitForTimeout(500);
|
|
await input.press("Meta+A");
|
|
await input.press("Backspace");
|
|
await page.waitForTimeout(300);
|
|
}
|
|
|
|
function log(label: string, report: LatencyReport) {
|
|
/* eslint-disable no-console */
|
|
console.log(`\n=== TYPING LATENCY: ${label} ===`);
|
|
console.log(`CPU throttle: ${THROTTLE_RATE}x`);
|
|
console.log(`input events >=16ms: ${report.count}`);
|
|
console.log(`median duration: ${report.median.toFixed(0)}ms`);
|
|
console.log(`p95 duration: ${report.p95.toFixed(0)}ms`);
|
|
console.log(`max duration: ${report.max.toFixed(0)}ms`);
|
|
console.log(`keystrokes >50ms: ${report.over50}`);
|
|
console.log(`longtask total: ${report.longtaskTotal.toFixed(0)}ms`);
|
|
/* eslint-enable no-console */
|
|
}
|
|
|
|
test("MEASURE: composer keystroke latency, quiet vs agent-busy channel", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(240_000);
|
|
await installMockBridge(page);
|
|
await page.goto("/");
|
|
await page.waitForFunction(
|
|
() => typeof window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function",
|
|
);
|
|
|
|
await page.addInitScript(() => {
|
|
const store = window as unknown as {
|
|
__INPUT_EVENTS__?: number[];
|
|
__LONGTASKS__?: number[];
|
|
};
|
|
store.__INPUT_EVENTS__ = [];
|
|
store.__LONGTASKS__ = [];
|
|
new PerformanceObserver((list) => {
|
|
for (const entry of list.getEntries()) {
|
|
if (entry.name === "input" || entry.name === "keydown") {
|
|
store.__INPUT_EVENTS__?.push(entry.duration);
|
|
}
|
|
}
|
|
}).observe({ type: "event", buffered: true, durationThreshold: 16 });
|
|
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 { __INPUT_EVENTS__?: number[] }).__INPUT_EVENTS__,
|
|
),
|
|
);
|
|
|
|
// The `agents` channel: agent members, realistic surface for the report.
|
|
await page.getByTestId("channel-agents").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("agents");
|
|
await expect(
|
|
page.getByTestId("message-timeline").locator("[data-message-id]").first(),
|
|
).toBeVisible();
|
|
|
|
const client = await page.context().newCDPSession(page);
|
|
await client.send("Emulation.setCPUThrottlingRate", { rate: THROTTLE_RATE });
|
|
|
|
// Warmup burst (JIT, first-render costs), unmeasured.
|
|
await typeBurst(page);
|
|
|
|
// ---- Scenario A: quiet ----
|
|
await resetWindowMetrics(page);
|
|
await typeBurst(page);
|
|
const quiet = await readWindowMetrics(page);
|
|
log("quiet channel", quiet);
|
|
|
|
// ---- Scenario B: agent-busy ----
|
|
// Round-robin typing indicators from N synthetic agent pubkeys, plus a
|
|
// live markdown message every 2s — in-page timers so the traffic keeps
|
|
// flowing while pressSequentially runs.
|
|
await page.evaluate(
|
|
({ agentCount, typingIntervalMs, messageIntervalMs }) => {
|
|
const w = window as unknown as {
|
|
__BUZZ_E2E_EMIT_MOCK_TYPING__?: (input: {
|
|
channelName: string;
|
|
pubkey?: string;
|
|
}) => unknown;
|
|
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
|
channelName: string;
|
|
content: string;
|
|
}) => unknown;
|
|
__BUSY_TIMERS__?: number[];
|
|
};
|
|
const pubkeys = Array.from({ length: agentCount }, (_, index) =>
|
|
`a${index}`.repeat(32),
|
|
);
|
|
let tick = 0;
|
|
const typingTimer = window.setInterval(() => {
|
|
tick += 1;
|
|
w.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({
|
|
channelName: "agents",
|
|
pubkey: pubkeys[tick % pubkeys.length],
|
|
});
|
|
}, typingIntervalMs);
|
|
let messageIndex = 0;
|
|
const messageTimer = window.setInterval(() => {
|
|
messageIndex += 1;
|
|
w.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "agents",
|
|
content: `**Progress ${messageIndex}**\n\n- step done\n- \`cargo check\` ok`,
|
|
});
|
|
}, messageIntervalMs);
|
|
w.__BUSY_TIMERS__ = [typingTimer, messageTimer];
|
|
},
|
|
{
|
|
agentCount: AGENT_COUNT,
|
|
typingIntervalMs: TYPING_EMIT_INTERVAL_MS,
|
|
messageIntervalMs: LIVE_MESSAGE_INTERVAL_MS,
|
|
},
|
|
);
|
|
// Let the busy traffic reach steady state (typing TTLs, working badges).
|
|
await page.waitForTimeout(2000);
|
|
|
|
await resetWindowMetrics(page);
|
|
await typeBurst(page);
|
|
const busy = await readWindowMetrics(page);
|
|
log(`agent-busy (${AGENT_COUNT} agents typing + live messages)`, busy);
|
|
|
|
await page.evaluate(() => {
|
|
const w = window as unknown as { __BUSY_TIMERS__?: number[] };
|
|
for (const timer of w.__BUSY_TIMERS__ ?? []) {
|
|
window.clearInterval(timer);
|
|
}
|
|
});
|
|
|
|
// ---- Scenario C: observer-frame storm ----
|
|
// Pre-fill 6 agents' observer buffers (1200 events each), then append one
|
|
// frame every 100ms round-robin. Each append runs the production ingestion
|
|
// path: O(E log E) buffer re-sort + the syncAll bridge's O(agents x
|
|
// events) rescan (see observerRelayStore.appendAgentEvent /
|
|
// activeAgentTurnsStore syncAll) — the suspected typing-lag source.
|
|
await page.evaluate(
|
|
({ agentCount, bufferSize, channelId }) => {
|
|
const w = window as unknown as {
|
|
__BUZZ_E2E_SEED_OBSERVER_EVENTS__?: (input: {
|
|
agentPubkey: string;
|
|
events: unknown[];
|
|
}) => void;
|
|
__OBS_AGENTS__?: Array<{ pubkey: string; seq: number }>;
|
|
};
|
|
const base = Date.parse("2025-06-15T12:00:00Z");
|
|
const makeEvent = (pubkey: string, seq: number) => ({
|
|
seq,
|
|
timestamp: new Date(base + seq * 1000).toISOString(),
|
|
kind: "acp_write",
|
|
agentIndex: 0,
|
|
channelId,
|
|
sessionId: `sess-${pubkey.slice(0, 4)}`,
|
|
turnId: `turn-${pubkey.slice(0, 4)}`,
|
|
payload: {
|
|
jsonrpc: "2.0",
|
|
method: "session/update",
|
|
params: { note: `event ${seq}` },
|
|
},
|
|
});
|
|
w.__OBS_AGENTS__ = Array.from({ length: agentCount }, (_, index) => ({
|
|
pubkey: `b${index}`.repeat(32),
|
|
seq: bufferSize,
|
|
}));
|
|
for (const agent of w.__OBS_AGENTS__) {
|
|
w.__BUZZ_E2E_SEED_OBSERVER_EVENTS__?.({
|
|
agentPubkey: agent.pubkey,
|
|
events: Array.from({ length: bufferSize }, (_, seq) =>
|
|
makeEvent(agent.pubkey, seq),
|
|
),
|
|
});
|
|
}
|
|
let stormTick = 0;
|
|
const stormTimer = window.setInterval(() => {
|
|
const agents = w.__OBS_AGENTS__ ?? [];
|
|
if (agents.length === 0) return;
|
|
stormTick += 1;
|
|
const agent = agents[stormTick % agents.length];
|
|
agent.seq += 1;
|
|
w.__BUZZ_E2E_SEED_OBSERVER_EVENTS__?.({
|
|
agentPubkey: agent.pubkey,
|
|
events: [makeEvent(agent.pubkey, agent.seq)],
|
|
});
|
|
}, 100);
|
|
(w as unknown as { __STORM_TIMER__?: number }).__STORM_TIMER__ =
|
|
stormTimer;
|
|
},
|
|
{
|
|
agentCount: 6,
|
|
bufferSize: 1200,
|
|
channelId: "94a444a4-c0a3-5966-ab05-530c6ddc2301", // #agents
|
|
},
|
|
);
|
|
await page.waitForTimeout(1500);
|
|
|
|
await resetWindowMetrics(page);
|
|
await typeBurst(page);
|
|
const storm = await readWindowMetrics(page);
|
|
log("observer storm (6 agents, 1200-event buffers, 10 frames/s)", storm);
|
|
|
|
// ---- Scenario D: everything at once (storm + typing + messages) ----
|
|
await page.evaluate(
|
|
({ agentCount, typingIntervalMs, messageIntervalMs }) => {
|
|
const w = window as unknown as {
|
|
__BUZZ_E2E_EMIT_MOCK_TYPING__?: (input: {
|
|
channelName: string;
|
|
pubkey?: string;
|
|
}) => unknown;
|
|
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
|
channelName: string;
|
|
content: string;
|
|
}) => unknown;
|
|
__BUSY_TIMERS__?: number[];
|
|
};
|
|
const pubkeys = Array.from({ length: agentCount }, (_, index) =>
|
|
`a${index}`.repeat(32),
|
|
);
|
|
let tick = 0;
|
|
const typingTimer = window.setInterval(() => {
|
|
tick += 1;
|
|
w.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({
|
|
channelName: "agents",
|
|
pubkey: pubkeys[tick % pubkeys.length],
|
|
});
|
|
}, typingIntervalMs);
|
|
let messageIndex = 0;
|
|
const messageTimer = window.setInterval(() => {
|
|
messageIndex += 1;
|
|
w.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "agents",
|
|
content: `**Progress ${messageIndex}**\n\n- step done\n- \`cargo check\` ok`,
|
|
});
|
|
}, messageIntervalMs);
|
|
w.__BUSY_TIMERS__ = [typingTimer, messageTimer];
|
|
},
|
|
{
|
|
agentCount: AGENT_COUNT,
|
|
typingIntervalMs: TYPING_EMIT_INTERVAL_MS,
|
|
messageIntervalMs: LIVE_MESSAGE_INTERVAL_MS,
|
|
},
|
|
);
|
|
await page.waitForTimeout(1000);
|
|
|
|
await resetWindowMetrics(page);
|
|
await typeBurst(page);
|
|
const everything = await readWindowMetrics(page);
|
|
log("storm + typing + messages", everything);
|
|
|
|
await page.evaluate(() => {
|
|
const w = window as unknown as {
|
|
__BUSY_TIMERS__?: number[];
|
|
__STORM_TIMER__?: number;
|
|
};
|
|
for (const timer of w.__BUSY_TIMERS__ ?? []) {
|
|
window.clearInterval(timer);
|
|
}
|
|
if (w.__STORM_TIMER__) window.clearInterval(w.__STORM_TIMER__);
|
|
});
|
|
|
|
// ---- Scenario E: streaming agent into a full markdown timeline ----
|
|
// The one O(rows) path is message-content change: each kind-40003 edit
|
|
// re-runs formatTimelineMessages over the whole window and re-parses the
|
|
// edited row's (growing) markdown. Agents streaming replies emit exactly
|
|
// this shape at high frequency. Seed a realistic timeline first.
|
|
await page.evaluate(
|
|
({ rows }) => {
|
|
const w = window as unknown as {
|
|
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
|
channelName: string;
|
|
content: string;
|
|
createdAt?: number;
|
|
}) => { id: string };
|
|
};
|
|
const base = Math.floor(Date.now() / 1000) - rows - 30;
|
|
for (let index = 0; index < rows; index += 1) {
|
|
w.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "agents",
|
|
content: [
|
|
`**Task ${index}** update from the build agent`,
|
|
"```rust",
|
|
`fn step_${index}() -> Result<(), Error> { run(${index}) }`,
|
|
"```",
|
|
`- [x] compile ${index}`,
|
|
`- see [logs](https://example.com/${index})`,
|
|
].join("\n"),
|
|
createdAt: base + index,
|
|
});
|
|
}
|
|
},
|
|
{ rows: 50 },
|
|
);
|
|
await page.waitForTimeout(1500);
|
|
|
|
await page.evaluate(
|
|
({ streamCount, prefillLines }) => {
|
|
const w = window as unknown as {
|
|
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
|
channelName: string;
|
|
content: string;
|
|
kind?: number;
|
|
extraTags?: string[][];
|
|
}) => { id: string };
|
|
__STREAM_TIMERS__?: number[];
|
|
};
|
|
w.__STREAM_TIMERS__ = [];
|
|
for (let stream = 0; stream < streamCount; stream += 1) {
|
|
const target = w.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "agents",
|
|
content: "Working on it…",
|
|
});
|
|
if (!target) continue;
|
|
// Pre-grown reply: mid-stream in a long code fence, the realistic
|
|
// worst case — every edit re-parses the whole accumulated content.
|
|
const chunks: string[] = ["**Streaming reply**", "", "```ts"];
|
|
for (let line = 0; line < prefillLines; line += 1) {
|
|
chunks.push(`const step${line} = await runStep(${line});`);
|
|
}
|
|
let line = prefillLines;
|
|
const streamTimer = window.setInterval(() => {
|
|
line += 1;
|
|
chunks.push(`const step${line} = await runStep(${line}); // live`);
|
|
w.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "agents",
|
|
content: `${chunks.join("\n")}\n\`\`\``,
|
|
kind: 40003,
|
|
extraTags: [["e", target.id]],
|
|
});
|
|
}, 120);
|
|
w.__STREAM_TIMERS__?.push(streamTimer);
|
|
}
|
|
},
|
|
{ streamCount: 3, prefillLines: 150 },
|
|
);
|
|
await page.waitForTimeout(1000);
|
|
|
|
await resetWindowMetrics(page);
|
|
await typeBurst(page);
|
|
const streaming = await readWindowMetrics(page);
|
|
log(
|
|
"3 streaming agents (150-line replies, edit every 120ms each)",
|
|
streaming,
|
|
);
|
|
|
|
await page.evaluate(() => {
|
|
const w = window as unknown as { __STREAM_TIMERS__?: number[] };
|
|
for (const timer of w.__STREAM_TIMERS__ ?? []) {
|
|
window.clearInterval(timer);
|
|
}
|
|
});
|
|
|
|
// ---- Scenario F: typing in a long thread (the real-world repro) ----
|
|
// Field report: a 68-reply thread with active agents is near-unusable to
|
|
// type in (median 408ms, p95 8s+ per keystroke in WKWebView), while a
|
|
// fresh thread in the same channel is instant — cost scales with the open
|
|
// thread's reply count. Seed a 68-reply thread, open it, and type; then
|
|
// repeat with agents streaming replies into the thread.
|
|
const rootId = await page.evaluate(() => {
|
|
const w = window as unknown as {
|
|
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
|
channelName: string;
|
|
content: string;
|
|
parentEventId?: string;
|
|
createdAt?: number;
|
|
}) => { id: string };
|
|
};
|
|
const base = Math.floor(Date.now() / 1000) - 300;
|
|
const root = w.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "agents",
|
|
content: "**Deploy thread** — agents report here",
|
|
createdAt: base,
|
|
});
|
|
if (!root) return null;
|
|
for (let index = 0; index < 68; index += 1) {
|
|
w.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "agents",
|
|
content: [
|
|
`**Reply ${index}** from agent`,
|
|
"```rust",
|
|
`fn check_${index}() -> bool { ${index} % 2 == 0 }`,
|
|
"```",
|
|
`- [x] validated step ${index}`,
|
|
].join("\n"),
|
|
parentEventId: root.id,
|
|
createdAt: base + index + 1,
|
|
});
|
|
}
|
|
return root.id;
|
|
});
|
|
expect(rootId).not.toBeNull();
|
|
await page.waitForTimeout(1500);
|
|
|
|
const threadSummary = page.getByTestId("message-thread-summary").last();
|
|
await threadSummary.scrollIntoViewIfNeeded();
|
|
await threadSummary.click();
|
|
const threadPanel = page.getByTestId("message-thread-panel");
|
|
await expect(threadPanel).toBeVisible();
|
|
await expect(threadPanel.getByTestId("message-row").first()).toBeVisible();
|
|
await page.waitForTimeout(1000);
|
|
|
|
// F1: long thread, no traffic.
|
|
await typeBurst(page, threadPanel); // warmup
|
|
await resetWindowMetrics(page);
|
|
await typeBurst(page, threadPanel);
|
|
const threadQuiet = await readWindowMetrics(page);
|
|
log("68-reply thread, quiet", threadQuiet);
|
|
|
|
// F2: agents streaming into the open thread while typing.
|
|
await page.evaluate(
|
|
({ rootEventId }) => {
|
|
const w = window as unknown as {
|
|
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
|
channelName: string;
|
|
content: string;
|
|
parentEventId?: string;
|
|
kind?: number;
|
|
extraTags?: string[][];
|
|
}) => { id: string };
|
|
__THREAD_TIMERS__?: number[];
|
|
};
|
|
// A new agent reply lands every 600ms…
|
|
let replyIndex = 0;
|
|
let lastReplyId: string | null = null;
|
|
const replyTimer = window.setInterval(() => {
|
|
replyIndex += 1;
|
|
const reply = w.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "agents",
|
|
content: `**Live reply ${replyIndex}**\n\n- working…`,
|
|
parentEventId: rootEventId ?? undefined,
|
|
});
|
|
lastReplyId = reply?.id ?? lastReplyId;
|
|
}, 600);
|
|
// …and the latest reply streams edits every 150ms.
|
|
const chunks: string[] = ["**Live reply**", "", "```ts"];
|
|
let line = 0;
|
|
const editTimer = window.setInterval(() => {
|
|
if (!lastReplyId) return;
|
|
line += 1;
|
|
chunks.push(`const s${line} = await step(${line});`);
|
|
w.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "agents",
|
|
content: `${chunks.join("\n")}\n\`\`\``,
|
|
kind: 40003,
|
|
extraTags: [["e", lastReplyId]],
|
|
});
|
|
}, 150);
|
|
w.__THREAD_TIMERS__ = [replyTimer, editTimer];
|
|
},
|
|
{ rootEventId: rootId },
|
|
);
|
|
await page.waitForTimeout(1000);
|
|
|
|
await resetWindowMetrics(page);
|
|
await typeBurst(page, threadPanel);
|
|
const threadBusy = await readWindowMetrics(page);
|
|
log("68-reply thread + agents streaming into it", threadBusy);
|
|
|
|
await page.evaluate(() => {
|
|
const w = window as unknown as { __THREAD_TIMERS__?: number[] };
|
|
for (const timer of w.__THREAD_TIMERS__ ?? []) {
|
|
window.clearInterval(timer);
|
|
}
|
|
});
|
|
await client.send("Emulation.setCPUThrottlingRate", { rate: 1 });
|
|
|
|
/* eslint-disable no-console */
|
|
console.log("\n=== SUMMARY (median / p95 / >50ms count / longtask) ===");
|
|
for (const [label, report] of [
|
|
["quiet ", quiet],
|
|
["busy ", busy],
|
|
["storm ", storm],
|
|
["everything", everything],
|
|
["streaming ", streaming],
|
|
["thread68 ", threadQuiet],
|
|
["thread68+ ", threadBusy],
|
|
] as const) {
|
|
console.log(
|
|
`${label}: ${report.median.toFixed(0)} / ${report.p95.toFixed(0)} / ${report.over50} / ${report.longtaskTotal.toFixed(0)}ms`,
|
|
);
|
|
}
|
|
/* eslint-enable no-console */
|
|
|
|
// Instrument, not a gate: confirm the harness measured real keystrokes.
|
|
expect(quiet.count + busy.count + storm.count).toBeGreaterThan(0);
|
|
});
|