← back to Open Seo
e2e/domain-overview-filters.perf.spec.ts
267 lines
import { expect, test } from "@playwright/test";
import {
attachDomainPerfMetrics,
attachJsonArtifact,
applyFilters,
closeFilters,
ensureFiltersOpen,
expectPageResponsive,
getDomainPerfMetrics,
installDomainPerfProbe,
openDomainOverview,
openFilters,
resetDomainPerfMetrics,
switchDomainTab,
type DomainPerfMetrics,
typeIntoDraftInput,
waitForDomainRows,
} from "./domain-overview-test-utils";
const CPU_THROTTLE_RATE = Number(process.env.DOMAIN_FILTER_CPU_THROTTLE ?? 6);
const PERF_BUDGETS = {
actionMs: Number(process.env.DOMAIN_FILTER_ACTION_MS ?? 2_500),
maxLongTaskMs: Number(process.env.DOMAIN_FILTER_MAX_LONG_TASK_MS ?? 1_000),
maxRafGapMs: Number(process.env.DOMAIN_FILTER_MAX_RAF_GAP_MS ?? 1_500),
maxInputMs: Number(process.env.DOMAIN_FILTER_MAX_INPUT_MS ?? 4_000),
totalLongTaskMs: Number(
process.env.DOMAIN_FILTER_TOTAL_LONG_TASK_MS ?? 8_000,
),
};
type PerfCheckpoint =
| {
label: string;
metrics: DomainPerfMetrics;
}
| {
label: string;
error: string;
};
type DomainDebugEntry = {
type: string;
text: string;
parsed: unknown;
};
test.describe("Domain Overview filter performance", () => {
test("captures main-thread stalls in the applied-filter edit flow", async ({
page,
browserName,
}, testInfo) => {
test.skip(browserName !== "chromium", "CPU throttling requires CDP");
test.setTimeout(120_000);
await installDomainPerfProbe(page);
await page.addInitScript(() => {
window.localStorage.setItem("debug:domain-overview", "1");
});
const domainDebugLog: DomainDebugEntry[] = [];
page.on("console", (message) => {
const text = message.text();
if (!text.startsWith("[domain-debug]")) return;
const raw = text.slice("[domain-debug]".length).trim();
domainDebugLog.push({
type: message.type(),
text,
parsed: parseDomainDebugMessage(raw),
});
});
const client = await page.context().newCDPSession(page);
await client.send("Emulation.setCPUThrottlingRate", {
rate: CPU_THROTTLE_RATE,
});
const checkpoints: PerfCheckpoint[] = [];
let finalMetrics: DomainPerfMetrics | null = null;
let flowError: unknown;
const captureCheckpoint = async (label: string) => {
checkpoints.push({
label,
metrics: await withDeadline(
getDomainPerfMetrics(page),
2_000,
`Timed out reading perf metrics for ${label}`,
),
});
};
try {
await openDomainOverview(page, "pages");
await waitForDomainRows(page, "Top Pages");
await resetDomainPerfMetrics(page);
await captureCheckpoint("ready on Top Pages");
await openFilters(page);
await closeFilters(page);
await openFilters(page);
await closeFilters(page);
await openFilters(page);
await captureCheckpoint("toggled Top Pages filters");
await typeIntoDraftInput(
page,
page.getByPlaceholder("Min").nth(0),
"10",
"Pages Traffic min",
{
actionTimeoutMs: PERF_BUDGETS.actionMs,
cdpSession: client,
inputLatencyBudgetMs: PERF_BUDGETS.maxInputMs,
recordPerf: true,
},
);
await applyFilters(page, "pMinTraffic", "10");
await captureCheckpoint("applied Pages Traffic min");
await ensureFiltersOpen(page, "Include Page Terms");
await typeIntoDraftInput(
page,
page.getByPlaceholder("Max").nth(1),
"50",
"Pages Keywords max",
{
actionTimeoutMs: PERF_BUDGETS.actionMs,
cdpSession: client,
inputLatencyBudgetMs: PERF_BUDGETS.maxInputMs,
recordPerf: true,
},
);
await expect(page.getByText("unapplied")).toBeVisible();
await expectPageResponsive(page, "after editing Pages Keywords max");
await captureCheckpoint("edited Pages Keywords max");
await switchDomainTab(page, "keywords");
await waitForDomainRows(page, "Top Keywords");
await openFilters(page);
await typeIntoDraftInput(
page,
page.getByPlaceholder("Max").nth(1),
"5000",
"Keywords Volume max",
{
actionTimeoutMs: PERF_BUDGETS.actionMs,
cdpSession: client,
inputLatencyBudgetMs: PERF_BUDGETS.maxInputMs,
recordPerf: true,
},
);
await expect(page.getByText("unapplied")).toBeVisible();
await expectPageResponsive(page, "after editing Keywords Volume max");
} catch (error) {
flowError = error;
checkpoints.push({
label: "flow error",
error: getErrorMessage(error),
});
} finally {
if (flowError) {
checkpoints.push({
label: "final metrics",
error:
"Skipped after renderer stall to preserve the original failure",
});
} else {
finalMetrics = await withDeadline(
getDomainPerfMetrics(page),
2_000,
"Timed out reading final perf metrics",
).catch((error: unknown) => {
checkpoints.push({
label: "final metrics",
error: getErrorMessage(error),
});
return null;
});
}
await attachJsonArtifact(
testInfo,
"domain-filter-perf-checkpoints.json",
checkpoints,
);
await attachJsonArtifact(
testInfo,
"domain-filter-debug-log.json",
domainDebugLog,
);
if (finalMetrics) {
await attachDomainPerfMetrics(testInfo, finalMetrics);
}
await withDeadline(
client.send("Emulation.setCPUThrottlingRate", { rate: 1 }),
2_000,
"Timed out resetting CPU throttling",
).catch(() => undefined);
}
if (flowError) throw flowError;
if (!finalMetrics) throw new Error("Perf metrics were not collected");
console.info(
"[domain-filter-perf]",
JSON.stringify(summarizeMetrics(finalMetrics)),
);
expect(finalMetrics.errors, "browser console/runtime errors").toEqual([]);
expect(
finalMetrics.maxInputDuration,
"slowest measured input",
).toBeLessThan(PERF_BUDGETS.maxInputMs);
expect(finalMetrics.maxLongTaskDuration, "slowest long task").toBeLessThan(
PERF_BUDGETS.maxLongTaskMs,
);
expect(
finalMetrics.totalLongTaskDuration,
"total long-task time",
).toBeLessThan(PERF_BUDGETS.totalLongTaskMs);
expect(finalMetrics.maxRafGap, "largest animation-frame gap").toBeLessThan(
PERF_BUDGETS.maxRafGapMs,
);
});
});
function summarizeMetrics(metrics: DomainPerfMetrics) {
return {
maxInputDuration: Math.round(metrics.maxInputDuration),
maxLongTaskDuration: Math.round(metrics.maxLongTaskDuration),
totalLongTaskDuration: Math.round(metrics.totalLongTaskDuration),
maxRafGap: Math.round(metrics.maxRafGap),
longTaskCount: metrics.longTasks.length,
rafGapCount: metrics.rafGaps.length,
inputEvents: metrics.inputEvents.map((event) => ({
label: event.label,
duration: Math.round(event.duration),
})),
};
}
function getErrorMessage(error: unknown) {
return error instanceof Error ? error.message : String(error);
}
function parseDomainDebugMessage(raw: string): unknown {
try {
return JSON.parse(raw);
} catch {
return raw;
}
}
async function withDeadline<T>(
promise: Promise<T>,
timeoutMs: number,
message: string,
): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
promise,
new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}