mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
perf(app): add explorer toggle profiling harness
This commit is contained in:
@@ -242,6 +242,19 @@ focused invalidation capture; React Native's generated stacks make that mode hig
|
||||
Set `PASEO_PROFILE_TRACE_FOCUS=1` to include focus targets, durations, and JavaScript call stacks in
|
||||
the scenario report. This mode wraps `HTMLElement.focus`, so use it only for diagnosis.
|
||||
|
||||
For the desktop explorer toggle, run the app against the root checkout's daemon and use:
|
||||
|
||||
```bash
|
||||
npm run profile:explorer-toggle --workspace=@getpaseo/app
|
||||
```
|
||||
|
||||
The harness verifies port `6768`, opens the Paseo workspace, creates and warms the explorer pane,
|
||||
records an idle control, then measures settled and 50 ms burst Cmd+E toggles. It reports
|
||||
input-to-DOM and input-to-paint latency, React commits, mounts, unmounts, and DOM mutations. Set
|
||||
`PASEO_PROFILE_TRACE_PATH=/tmp/explorer-toggle.trace.json` or
|
||||
`PASEO_PROFILE_CPU_PATH=/tmp/explorer-toggle.cpuprofile` for separate Chromium captures. Override
|
||||
the app URL, daemon port, workspace, or server with the corresponding `PASEO_PROFILE_*` variables.
|
||||
|
||||
For sustained composer typing, run the paired composer-versus-textarea benchmark against a seeded
|
||||
daemon:
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"build": "npm run build:web",
|
||||
"build:web": "npm --prefix ../.. run build:app-deps && expo export --platform web",
|
||||
"profile:composer-typing": "node ./scripts/profile-composer-typing.mjs",
|
||||
"profile:explorer-toggle": "node ./scripts/profile-explorer-toggle.mjs",
|
||||
"profile:workspace-switching": "node ./scripts/profile-workspace-switching.mjs",
|
||||
"profile:workspace-tabs": "node ./scripts/profile-workspace-tabs.mjs",
|
||||
"deploy:web": "npm run build:web && wrangler pages deploy dist --project-name paseo-app --branch main",
|
||||
|
||||
@@ -0,0 +1,538 @@
|
||||
import { connect } from "node:net";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const appUrl = process.env.PASEO_PROFILE_APP_URL ?? "http://127.0.0.1:8081";
|
||||
const daemonPort = Number(process.env.PASEO_PROFILE_DAEMON_PORT ?? 6768);
|
||||
const workspaceCwd = process.env.PASEO_PROFILE_WORKSPACE_CWD ?? repoRoot;
|
||||
const workspaceId = process.env.PASEO_PROFILE_WORKSPACE_ID ?? resolvePaseoWorkspaceId();
|
||||
const serverId =
|
||||
process.env.PASEO_PROFILE_SERVER_ID ??
|
||||
(await readFile(resolve(repoRoot, ".dev/paseo-home/server-id"), "utf8")).trim();
|
||||
const warmPresses = numberFromEnv("PASEO_PROFILE_WARM_PRESSES", 8);
|
||||
const measuredPresses = numberFromEnv("PASEO_PROFILE_MEASURED_PRESSES", 20);
|
||||
const burstPresses = numberFromEnv("PASEO_PROFILE_BURST_PRESSES", 20);
|
||||
const burstCadenceMs = numberFromEnv("PASEO_PROFILE_BURST_CADENCE_MS", 50);
|
||||
const idleBaselineMs = numberFromEnv("PASEO_PROFILE_IDLE_MS", 2_000);
|
||||
const tracePath = process.env.PASEO_PROFILE_TRACE_PATH?.trim() || null;
|
||||
const cpuProfilePath = process.env.PASEO_PROFILE_CPU_PATH?.trim() || null;
|
||||
const dumpCommits = process.env.PASEO_PROFILE_DUMP_COMMITS === "1";
|
||||
|
||||
function numberFromEnv(name, fallback) {
|
||||
const value = Number(process.env[name] ?? fallback);
|
||||
if (!Number.isFinite(value) || value < 0) {
|
||||
throw new Error(`${name} must be a non-negative number`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function resolvePaseoWorkspaceId() {
|
||||
const output = execFileSync("npm", ["run", "cli", "--", "workspace", "ls", "--json"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, FORCE_COLOR: "0" },
|
||||
});
|
||||
const jsonStart = output.indexOf("[\n");
|
||||
if (jsonStart < 0) throw new Error("Could not parse `paseo workspace ls --json`");
|
||||
const workspaces = JSON.parse(output.slice(jsonStart));
|
||||
const candidates = workspaces.filter((workspace) => workspace.cwd === workspaceCwd);
|
||||
const workspace = candidates.find((candidate) => candidate.name === "Paseo") ?? candidates[0];
|
||||
if (!workspace?.workspaceId) {
|
||||
throw new Error(`No active workspace found for ${workspaceCwd}`);
|
||||
}
|
||||
return workspace.workspaceId;
|
||||
}
|
||||
|
||||
function round(value) {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
|
||||
function percentile(values, fraction) {
|
||||
if (values.length === 0) return null;
|
||||
const sorted = [...values].sort((left, right) => left - right);
|
||||
return round(sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * fraction))]);
|
||||
}
|
||||
|
||||
function summarizeDurations(values) {
|
||||
return {
|
||||
count: values.length,
|
||||
minMs: values.length > 0 ? round(Math.min(...values)) : null,
|
||||
medianMs: percentile(values, 0.5),
|
||||
p95Ms: percentile(values, 0.95),
|
||||
maxMs: values.length > 0 ? round(Math.max(...values)) : null,
|
||||
};
|
||||
}
|
||||
|
||||
async function assertPortReachable(port) {
|
||||
await new Promise((resolveConnection, rejectConnection) => {
|
||||
const socket = connect({ host: "127.0.0.1", port });
|
||||
const timeout = setTimeout(() => {
|
||||
socket.destroy();
|
||||
rejectConnection(new Error(`Timed out connecting to daemon on 127.0.0.1:${port}`));
|
||||
}, 2_000);
|
||||
socket.once("connect", () => {
|
||||
clearTimeout(timeout);
|
||||
socket.destroy();
|
||||
resolveConnection();
|
||||
});
|
||||
socket.once("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
rejectConnection(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function installMeasurementProbe(page) {
|
||||
await page.evaluate(() => {
|
||||
const readExplorerState = () => {
|
||||
const tab = document.querySelector('[data-testid="workspace-tab-working_diff"]');
|
||||
const group = tab?.closest('[data-testid^="split-group-child"]');
|
||||
const testId = group?.getAttribute("data-testid");
|
||||
if (testId === "split-group-child-hidden") return "hidden";
|
||||
if (testId === "split-group-child") return "visible";
|
||||
return "absent";
|
||||
};
|
||||
const state = {
|
||||
events: [],
|
||||
domMutations: 0,
|
||||
addedNodes: 0,
|
||||
removedNodes: 0,
|
||||
explorerMounts: 0,
|
||||
explorerUnmounts: 0,
|
||||
explorerMounted: readExplorerState() !== "absent",
|
||||
};
|
||||
const settleLatestEvent = () => {
|
||||
const event = state.events.findLast((candidate) => candidate.paintTime === null);
|
||||
if (!event) return;
|
||||
const after = readExplorerState();
|
||||
if (after === event.before) return;
|
||||
event.after = after;
|
||||
event.mutationTime ??= performance.now();
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
event.paintTime ??= performance.now();
|
||||
});
|
||||
});
|
||||
};
|
||||
const observer = new MutationObserver((records) => {
|
||||
state.domMutations += records.length;
|
||||
for (const record of records) {
|
||||
state.addedNodes += record.addedNodes.length;
|
||||
state.removedNodes += record.removedNodes.length;
|
||||
}
|
||||
const explorerMounted = readExplorerState() !== "absent";
|
||||
if (!state.explorerMounted && explorerMounted) state.explorerMounts += 1;
|
||||
if (state.explorerMounted && !explorerMounted) state.explorerUnmounts += 1;
|
||||
state.explorerMounted = explorerMounted;
|
||||
settleLatestEvent();
|
||||
});
|
||||
observer.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-testid", "style"],
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
const onKeyDown = (event) => {
|
||||
if (!event.metaKey || event.code !== "KeyE") return;
|
||||
const sequence = state.events.length;
|
||||
performance.mark(`paseo:explorer-toggle:keydown:${sequence}`);
|
||||
console.timeStamp(`paseo:explorer-toggle:keydown:${sequence}`);
|
||||
state.events.push({
|
||||
sequence,
|
||||
inputTime: event.timeStamp,
|
||||
before: globalThis.__PASEO_EXPLORER_TOGGLE_PENDING_BEFORE__ ?? readExplorerState(),
|
||||
after: null,
|
||||
mutationTime: null,
|
||||
paintTime: null,
|
||||
});
|
||||
globalThis.__PASEO_EXPLORER_TOGGLE_PENDING_BEFORE__ = undefined;
|
||||
};
|
||||
addEventListener("keydown", onKeyDown, true);
|
||||
globalThis.__PASEO_EXPLORER_TOGGLE_PROFILE__ = state;
|
||||
globalThis.__PASEO_EXPLORER_TOGGLE_STATE__ = readExplorerState;
|
||||
globalThis.__PASEO_EXPLORER_TOGGLE_RESET__ = () => {
|
||||
state.events.length = 0;
|
||||
state.domMutations = 0;
|
||||
state.addedNodes = 0;
|
||||
state.removedNodes = 0;
|
||||
state.explorerMounts = 0;
|
||||
state.explorerUnmounts = 0;
|
||||
state.explorerMounted = readExplorerState() !== "absent";
|
||||
globalThis.__PASEO_RESET_RENDER_PROFILE__?.();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function explorerState(page) {
|
||||
return page.evaluate(() => globalThis.__PASEO_EXPLORER_TOGGLE_STATE__?.() ?? "absent");
|
||||
}
|
||||
|
||||
async function waitForProfilerIdle(page, quietMs = 500) {
|
||||
let previousCount = -1;
|
||||
let unchangedSince = Date.now();
|
||||
const deadline = Date.now() + 15_000;
|
||||
while (Date.now() < deadline) {
|
||||
const count = await page.evaluate(() => globalThis.__PASEO_RENDER_PROFILE__?.length ?? 0);
|
||||
if (count !== previousCount) {
|
||||
previousCount = count;
|
||||
unchangedSince = Date.now();
|
||||
} else if (Date.now() - unchangedSince >= quietMs) {
|
||||
return;
|
||||
}
|
||||
await page.waitForTimeout(50);
|
||||
}
|
||||
throw new Error("React profiler did not become idle");
|
||||
}
|
||||
|
||||
async function pressAndWaitForPaint(page) {
|
||||
const eventIndex = await page.evaluate(
|
||||
() => globalThis.__PASEO_EXPLORER_TOGGLE_PROFILE__?.events.length ?? 0,
|
||||
);
|
||||
const before = await explorerState(page);
|
||||
await page.evaluate((state) => {
|
||||
globalThis.__PASEO_EXPLORER_TOGGLE_PENDING_BEFORE__ = state;
|
||||
}, before);
|
||||
await page.keyboard.press("Meta+e");
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
(index) => {
|
||||
const event = globalThis.__PASEO_EXPLORER_TOGGLE_PROFILE__?.events[index];
|
||||
return event !== undefined && typeof event.paintTime === "number";
|
||||
},
|
||||
eventIndex,
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
} catch (error) {
|
||||
const diagnostic = await page.evaluate(() => ({
|
||||
state: globalThis.__PASEO_EXPLORER_TOGGLE_STATE__?.(),
|
||||
measurements: globalThis.__PASEO_EXPLORER_TOGGLE_PROFILE__,
|
||||
}));
|
||||
throw new Error(`Cmd+E did not paint: ${JSON.stringify(diagnostic)}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
async function pressBurst(page, count, cadenceMs) {
|
||||
const initialState = await explorerState(page);
|
||||
let expectedBefore = initialState;
|
||||
await page.keyboard.down("Meta");
|
||||
try {
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
await page.evaluate((state) => {
|
||||
globalThis.__PASEO_EXPLORER_TOGGLE_PENDING_BEFORE__ = state;
|
||||
}, expectedBefore);
|
||||
await page.keyboard.press("e");
|
||||
expectedBefore = expectedBefore === "visible" ? "hidden" : "visible";
|
||||
if (index + 1 < count) await page.waitForTimeout(cadenceMs);
|
||||
}
|
||||
} finally {
|
||||
await page.keyboard.up("Meta");
|
||||
}
|
||||
await page.waitForFunction(
|
||||
(expectedCount) => {
|
||||
const events = globalThis.__PASEO_EXPLORER_TOGGLE_PROFILE__?.events ?? [];
|
||||
return (
|
||||
events.length === expectedCount &&
|
||||
events.every((event) => typeof event.paintTime === "number")
|
||||
);
|
||||
},
|
||||
count,
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
const finalState = await explorerState(page);
|
||||
let expectedFinalState = initialState;
|
||||
if (count % 2 !== 0) {
|
||||
expectedFinalState = initialState === "visible" ? "hidden" : "visible";
|
||||
}
|
||||
if (finalState !== expectedFinalState) {
|
||||
throw new Error(
|
||||
`Cmd+E burst lost an input: expected ${expectedFinalState}, observed ${finalState}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function groupCommits(samples) {
|
||||
const commits = new Map();
|
||||
for (const sample of samples) {
|
||||
const commit = commits.get(sample.commitTime) ?? {
|
||||
commitTime: sample.commitTime,
|
||||
rootActualDuration: null,
|
||||
components: [],
|
||||
};
|
||||
if (sample.id === "WorkspaceDeck") commit.rootActualDuration = sample.actualDuration;
|
||||
commit.components.push({
|
||||
id: sample.id,
|
||||
phase: sample.phase,
|
||||
actualDuration: round(sample.actualDuration),
|
||||
baseDuration: round(sample.baseDuration),
|
||||
});
|
||||
commits.set(sample.commitTime, commit);
|
||||
}
|
||||
return [...commits.values()]
|
||||
.sort((left, right) => left.commitTime - right.commitTime)
|
||||
.map((commit) => {
|
||||
commit.commitTime = round(commit.commitTime);
|
||||
commit.rootActualDuration =
|
||||
commit.rootActualDuration === null ? null : round(commit.rootActualDuration);
|
||||
commit.components.sort((left, right) => right.actualDuration - left.actualDuration);
|
||||
return commit;
|
||||
});
|
||||
}
|
||||
|
||||
function summarizeComponents(samples) {
|
||||
const components = new Map();
|
||||
for (const sample of samples) {
|
||||
const current = components.get(sample.id) ?? {
|
||||
renders: 0,
|
||||
totalActualDuration: 0,
|
||||
durations: [],
|
||||
};
|
||||
current.renders += 1;
|
||||
current.totalActualDuration += sample.actualDuration;
|
||||
current.durations.push(sample.actualDuration);
|
||||
components.set(sample.id, current);
|
||||
}
|
||||
return [...components.entries()]
|
||||
.map(([id, value]) => ({
|
||||
id,
|
||||
renders: value.renders,
|
||||
totalActualDurationMs: round(value.totalActualDuration),
|
||||
medianActualDurationMs: percentile(value.durations, 0.5),
|
||||
p95ActualDurationMs: percentile(value.durations, 0.95),
|
||||
}))
|
||||
.sort((left, right) => right.totalActualDurationMs - left.totalActualDurationMs);
|
||||
}
|
||||
|
||||
async function readScenario(page, name) {
|
||||
const result = await page.evaluate(() => ({
|
||||
measurements: globalThis.__PASEO_EXPLORER_TOGGLE_PROFILE__,
|
||||
samples: globalThis.__PASEO_RENDER_PROFILE__ ?? [],
|
||||
reasons: globalThis.__PASEO_RENDER_PROFILE_REASONS__ ?? {},
|
||||
}));
|
||||
const events = result.measurements.events;
|
||||
const inputToMutation = events.map((event) => event.mutationTime - event.inputTime);
|
||||
const inputToPaint = events.map((event) => event.paintTime - event.inputTime);
|
||||
const commits = groupCommits(result.samples);
|
||||
return {
|
||||
name,
|
||||
inputCount: events.length,
|
||||
transitions: events.map((event) => `${event.before}->${event.after}`),
|
||||
inputToMutation: summarizeDurations(inputToMutation),
|
||||
inputToPaint: summarizeDurations(inputToPaint),
|
||||
react: {
|
||||
commitCount: commits.length,
|
||||
rootCommitDuration: summarizeDurations(
|
||||
commits.flatMap((commit) =>
|
||||
commit.rootActualDuration === null ? [] : [commit.rootActualDuration],
|
||||
),
|
||||
),
|
||||
components: summarizeComponents(result.samples),
|
||||
reasons: result.reasons,
|
||||
commits: dumpCommits ? commits : undefined,
|
||||
},
|
||||
dom: {
|
||||
mutationRecords: result.measurements.domMutations,
|
||||
addedNodes: result.measurements.addedNodes,
|
||||
removedNodes: result.measurements.removedNodes,
|
||||
explorerMounts: result.measurements.explorerMounts,
|
||||
explorerUnmounts: result.measurements.explorerUnmounts,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function resetMeasurements(page) {
|
||||
await page.evaluate(() => globalThis.__PASEO_EXPLORER_TOGGLE_RESET__?.());
|
||||
}
|
||||
|
||||
async function runSettledScenario(page, name, count) {
|
||||
await resetMeasurements(page);
|
||||
for (let index = 0; index < count; index += 1) await pressAndWaitForPaint(page);
|
||||
return readScenario(page, name);
|
||||
}
|
||||
|
||||
async function runIdleScenario(page) {
|
||||
await resetMeasurements(page);
|
||||
await page.waitForTimeout(idleBaselineMs);
|
||||
return readScenario(page, "idle-baseline");
|
||||
}
|
||||
|
||||
async function runBurstScenario(page, name, count) {
|
||||
await resetMeasurements(page);
|
||||
await pressBurst(page, count, burstCadenceMs);
|
||||
return readScenario(page, name);
|
||||
}
|
||||
|
||||
async function captureCpuProfile(page, run) {
|
||||
const session = await page.context().newCDPSession(page);
|
||||
await session.send("Profiler.enable");
|
||||
await session.send("Profiler.setSamplingInterval", { interval: 100 });
|
||||
await session.send("Profiler.start");
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
const { profile } = await session.send("Profiler.stop");
|
||||
await session.send("Profiler.disable");
|
||||
await writeFile(cpuProfilePath, JSON.stringify(profile));
|
||||
}
|
||||
}
|
||||
|
||||
async function collectMemory(session, page) {
|
||||
await session.send("HeapProfiler.collectGarbage");
|
||||
const [heap, performanceMetrics, explorerTabs] = await Promise.all([
|
||||
session.send("Runtime.getHeapUsage"),
|
||||
session.send("Performance.getMetrics"),
|
||||
page.locator('[data-testid="workspace-tab-working_diff"]').count(),
|
||||
]);
|
||||
const metrics = Object.fromEntries(
|
||||
performanceMetrics.metrics
|
||||
.filter((metric) => ["JSHeapUsedSize", "JSHeapTotalSize", "Nodes"].includes(metric.name))
|
||||
.map((metric) => [metric.name, round(metric.value)]),
|
||||
);
|
||||
return {
|
||||
usedSize: heap.usedSize,
|
||||
totalSize: heap.totalSize,
|
||||
embedderHeapUsedSize: heap.embedderHeapUsedSize,
|
||||
backingStorageSize: heap.backingStorageSize,
|
||||
domNodes: metrics.Nodes ?? null,
|
||||
explorerTabs,
|
||||
metrics,
|
||||
};
|
||||
}
|
||||
|
||||
async function readCdpStream(session, handle) {
|
||||
const chunks = [];
|
||||
while (true) {
|
||||
const result = await session.send("IO.read", { handle });
|
||||
chunks.push(
|
||||
result.base64Encoded ? Buffer.from(result.data, "base64") : Buffer.from(result.data),
|
||||
);
|
||||
if (result.eof) break;
|
||||
}
|
||||
await session.send("IO.close", { handle });
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
async function captureDevToolsTrace(page, run) {
|
||||
const session = await page.context().newCDPSession(page);
|
||||
const tracingComplete = new Promise((resolveTrace) => {
|
||||
session.once("Tracing.tracingComplete", resolveTrace);
|
||||
});
|
||||
await session.send("Tracing.start", {
|
||||
categories: [
|
||||
"-*",
|
||||
"toplevel",
|
||||
"devtools.timeline",
|
||||
"blink.user_timing",
|
||||
"latencyInfo",
|
||||
"disabled-by-default-devtools.timeline",
|
||||
"disabled-by-default-devtools.timeline.frame",
|
||||
"disabled-by-default-v8.cpu_profiler",
|
||||
"disabled-by-default-v8.cpu_profiler.hires",
|
||||
].join(","),
|
||||
options: "sampling-frequency=10000",
|
||||
transferMode: "ReturnAsStream",
|
||||
});
|
||||
let result;
|
||||
try {
|
||||
result = await run();
|
||||
} finally {
|
||||
await session.send("Tracing.end");
|
||||
const completed = await tracingComplete;
|
||||
await writeFile(tracePath, await readCdpStream(session, completed.stream));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await assertPortReachable(daemonPort);
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
try {
|
||||
await page.addInitScript(() => {
|
||||
const preserveRenderProfile = (original) =>
|
||||
function preserveProfileParam(state, unused, url) {
|
||||
if (url === undefined || url === null) {
|
||||
return Reflect.apply(original, this, [state, unused, url]);
|
||||
}
|
||||
const nextUrl = new URL(String(url), location.href);
|
||||
nextUrl.searchParams.set("renderProfile", "1");
|
||||
return Reflect.apply(original, this, [
|
||||
state,
|
||||
unused,
|
||||
`${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`,
|
||||
]);
|
||||
};
|
||||
history.pushState = preserveRenderProfile(history.pushState);
|
||||
history.replaceState = preserveRenderProfile(history.replaceState);
|
||||
});
|
||||
await page.goto(`${appUrl}/?renderProfile=1`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60_000,
|
||||
});
|
||||
const workspaceRow = page.getByTestId(`sidebar-workspace-row-${serverId}:${workspaceId}`);
|
||||
await workspaceRow.waitFor({ timeout: 60_000 });
|
||||
await workspaceRow.click();
|
||||
await page.getByTestId("workspace-tabs-row").first().waitFor({ timeout: 60_000 });
|
||||
await waitForProfilerIdle(page);
|
||||
await installMeasurementProbe(page);
|
||||
|
||||
if ((await explorerState(page)) === "absent") {
|
||||
await pressAndWaitForPaint(page);
|
||||
await waitForProfilerIdle(page);
|
||||
}
|
||||
for (let index = 0; index < warmPresses; index += 1) await pressAndWaitForPaint(page);
|
||||
|
||||
const memorySession = await page.context().newCDPSession(page);
|
||||
await memorySession.send("Performance.enable");
|
||||
const memoryBefore = await collectMemory(memorySession, page);
|
||||
const idle = await runIdleScenario(page);
|
||||
const settled = await runSettledScenario(page, "retained-settled", measuredPresses);
|
||||
const burst = await runBurstScenario(page, "retained-burst", burstPresses);
|
||||
const memoryAfter = await collectMemory(memorySession, page);
|
||||
await memorySession.send("Performance.disable");
|
||||
|
||||
let cpu = null;
|
||||
if (cpuProfilePath) {
|
||||
cpu = await captureCpuProfile(page, () =>
|
||||
runSettledScenario(page, "retained-settled-cpu", measuredPresses),
|
||||
);
|
||||
}
|
||||
let trace = null;
|
||||
if (tracePath) {
|
||||
trace = await captureDevToolsTrace(page, () =>
|
||||
runSettledScenario(page, "retained-settled-trace", measuredPresses),
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
appUrl,
|
||||
daemon: `127.0.0.1:${daemonPort}`,
|
||||
serverId,
|
||||
workspaceId,
|
||||
workspaceCwd,
|
||||
warmPresses,
|
||||
measuredPresses,
|
||||
burstPresses,
|
||||
burstCadenceMs,
|
||||
idleBaselineMs,
|
||||
cpuProfilePath,
|
||||
tracePath,
|
||||
memory: { before: memoryBefore, after: memoryAfter },
|
||||
scenarios: [idle, settled, burst],
|
||||
profileScenarios: { cpu, trace },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
Reference in New Issue
Block a user