mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
252 lines
7.7 KiB
JavaScript
252 lines
7.7 KiB
JavaScript
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { chromium } from 'playwright';
|
|
|
|
const url = process.argv[2] || 'https://rover.otter.land';
|
|
const outDir = process.argv[3] || path.resolve('perf/results');
|
|
const cpuThrottle = Number(process.env.CPU_THROTTLE || 1);
|
|
const viewport = (process.env.VIEWPORT || '1440x950').split('x').map((part) => Number(part));
|
|
const isMobile = process.env.MOBILE === '1';
|
|
const runId = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const runDir = path.join(outDir, runId);
|
|
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
function summarizeCpuProfile(profile) {
|
|
const nodes = new Map(profile.nodes.map((node) => [node.id, node]));
|
|
const totals = new Map();
|
|
const samples = profile.samples || [];
|
|
const deltas = profile.timeDeltas || [];
|
|
let totalUs = 0;
|
|
|
|
for (let i = 0; i < samples.length; i += 1) {
|
|
const delta = deltas[i] || 0;
|
|
totalUs += delta;
|
|
const node = nodes.get(samples[i]);
|
|
if (!node) continue;
|
|
const frame = node.callFrame || {};
|
|
const key = `${frame.url || '(anonymous)'}::${frame.functionName || '(anonymous)'}`;
|
|
const prev = totals.get(key) || {
|
|
url: frame.url || '',
|
|
functionName: frame.functionName || '(anonymous)',
|
|
lineNumber: frame.lineNumber,
|
|
columnNumber: frame.columnNumber,
|
|
selfMs: 0,
|
|
samples: 0,
|
|
};
|
|
prev.selfMs += delta / 1000;
|
|
prev.samples += 1;
|
|
totals.set(key, prev);
|
|
}
|
|
|
|
return {
|
|
totalProfileMs: totalUs / 1000,
|
|
sampleCount: samples.length,
|
|
topSelfTime: [...totals.values()]
|
|
.sort((a, b) => b.selfMs - a.selfMs)
|
|
.slice(0, 30),
|
|
};
|
|
}
|
|
|
|
function interestingMetrics(metrics) {
|
|
const wanted = [
|
|
'Timestamp',
|
|
'Documents',
|
|
'Frames',
|
|
'JSEventListeners',
|
|
'Nodes',
|
|
'LayoutCount',
|
|
'RecalcStyleCount',
|
|
'LayoutDuration',
|
|
'RecalcStyleDuration',
|
|
'ScriptDuration',
|
|
'TaskDuration',
|
|
'JSHeapUsedSize',
|
|
'JSHeapTotalSize',
|
|
];
|
|
return Object.fromEntries(
|
|
metrics.metrics
|
|
.filter((metric) => wanted.includes(metric.name))
|
|
.map((metric) => [metric.name, metric.value]),
|
|
);
|
|
}
|
|
|
|
async function performInteractions(page) {
|
|
await page.mouse.wheel(0, 1400);
|
|
await sleep(800);
|
|
await page.mouse.wheel(0, -900);
|
|
await sleep(800);
|
|
await page.keyboard.press('Tab');
|
|
await sleep(300);
|
|
await page.keyboard.press('Tab');
|
|
await sleep(300);
|
|
|
|
const buttons = await page.locator('button:visible').elementHandles();
|
|
for (const button of buttons.slice(0, 3)) {
|
|
const box = await button.boundingBox();
|
|
if (!box || box.width < 8 || box.height < 8) continue;
|
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
await sleep(150);
|
|
}
|
|
}
|
|
|
|
async function run() {
|
|
await fs.mkdir(runDir, { recursive: true });
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
args: [
|
|
'--autoplay-policy=no-user-gesture-required',
|
|
'--disable-background-timer-throttling',
|
|
'--disable-renderer-backgrounding',
|
|
],
|
|
});
|
|
const context = await browser.newContext({
|
|
viewport: { width: viewport[0] || 1440, height: viewport[1] || 950 },
|
|
deviceScaleFactor: 1,
|
|
isMobile,
|
|
hasTouch: isMobile,
|
|
ignoreHTTPSErrors: true,
|
|
});
|
|
|
|
await context.addInitScript(() => {
|
|
window.__perfProbe = {
|
|
longTasks: [],
|
|
frames: [],
|
|
intervalDrifts: [],
|
|
};
|
|
|
|
try {
|
|
new PerformanceObserver((list) => {
|
|
for (const entry of list.getEntries()) {
|
|
window.__perfProbe.longTasks.push({
|
|
name: entry.name,
|
|
startTime: entry.startTime,
|
|
duration: entry.duration,
|
|
});
|
|
}
|
|
}).observe({ type: 'longtask', buffered: true });
|
|
} catch {}
|
|
|
|
let lastFrame = performance.now();
|
|
const frameLoop = (now) => {
|
|
window.__perfProbe.frames.push(now - lastFrame);
|
|
lastFrame = now;
|
|
requestAnimationFrame(frameLoop);
|
|
};
|
|
requestAnimationFrame(frameLoop);
|
|
|
|
let lastTick = performance.now();
|
|
setInterval(() => {
|
|
const now = performance.now();
|
|
window.__perfProbe.intervalDrifts.push(now - lastTick - 250);
|
|
lastTick = now;
|
|
}, 250);
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
const consoleMessages = [];
|
|
const failedRequests = [];
|
|
const responses = [];
|
|
|
|
page.on('console', (message) => {
|
|
consoleMessages.push({
|
|
type: message.type(),
|
|
text: message.text().slice(0, 1000),
|
|
location: message.location(),
|
|
});
|
|
});
|
|
page.on('requestfailed', (request) => {
|
|
failedRequests.push({
|
|
url: request.url(),
|
|
method: request.method(),
|
|
failure: request.failure()?.errorText,
|
|
});
|
|
});
|
|
page.on('response', (response) => {
|
|
const status = response.status();
|
|
if (status >= 400) responses.push({ url: response.url(), status });
|
|
});
|
|
|
|
const cdp = await context.newCDPSession(page);
|
|
await cdp.send('Performance.enable');
|
|
await cdp.send('Profiler.enable');
|
|
if (cpuThrottle > 1) {
|
|
await cdp.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottle });
|
|
}
|
|
|
|
const start = Date.now();
|
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
await page.waitForLoadState('load', { timeout: 60000 }).catch(() => {});
|
|
await sleep(5000);
|
|
const afterLoadMetrics = interestingMetrics(await cdp.send('Performance.getMetrics'));
|
|
await page.screenshot({ path: path.join(runDir, 'loaded.png'), fullPage: true });
|
|
|
|
await cdp.send('Profiler.start');
|
|
await sleep(15000);
|
|
await performInteractions(page);
|
|
await sleep(15000);
|
|
const { profile } = await cdp.send('Profiler.stop');
|
|
const endMetrics = interestingMetrics(await cdp.send('Performance.getMetrics'));
|
|
|
|
const perfProbe = await page.evaluate(() => window.__perfProbe);
|
|
await page.screenshot({ path: path.join(runDir, 'after-profile.png'), fullPage: true });
|
|
|
|
const cpuSummary = summarizeCpuProfile(profile);
|
|
const frameDeltas = perfProbe.frames.slice(5);
|
|
const longTasks = perfProbe.longTasks.sort((a, b) => b.duration - a.duration);
|
|
const intervalDrifts = perfProbe.intervalDrifts;
|
|
const percentile = (values, p) => {
|
|
if (!values.length) return 0;
|
|
const sorted = [...values].sort((a, b) => a - b);
|
|
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
|
|
};
|
|
|
|
const report = {
|
|
url,
|
|
runDir,
|
|
elapsedMs: Date.now() - start,
|
|
userAgent: await page.evaluate(() => navigator.userAgent),
|
|
title: await page.title(),
|
|
finalUrl: page.url(),
|
|
environment: {
|
|
cpuThrottle,
|
|
viewport: { width: viewport[0] || 1440, height: viewport[1] || 950 },
|
|
isMobile,
|
|
},
|
|
metrics: {
|
|
afterLoad: afterLoadMetrics,
|
|
end: endMetrics,
|
|
delta: Object.fromEntries(
|
|
Object.keys(endMetrics).map((key) => [key, endMetrics[key] - (afterLoadMetrics[key] || 0)]),
|
|
),
|
|
},
|
|
cpuSummary,
|
|
responsiveness: {
|
|
longTaskCount: longTasks.length,
|
|
topLongTasks: longTasks.slice(0, 20),
|
|
frameCount: frameDeltas.length,
|
|
averageFrameMs: frameDeltas.reduce((sum, value) => sum + value, 0) / frameDeltas.length,
|
|
p95FrameMs: percentile(frameDeltas, 0.95),
|
|
p99FrameMs: percentile(frameDeltas, 0.99),
|
|
intervalDriftCount: intervalDrifts.length,
|
|
averageIntervalDriftMs:
|
|
intervalDrifts.reduce((sum, value) => sum + value, 0) / intervalDrifts.length,
|
|
p95IntervalDriftMs: percentile(intervalDrifts, 0.95),
|
|
p99IntervalDriftMs: percentile(intervalDrifts, 0.99),
|
|
},
|
|
consoleMessages,
|
|
failedRequests,
|
|
errorResponses: responses,
|
|
};
|
|
|
|
await fs.writeFile(path.join(runDir, 'cpu-profile.cpuprofile'), JSON.stringify(profile));
|
|
await fs.writeFile(path.join(runDir, 'report.json'), JSON.stringify(report, null, 2));
|
|
console.log(JSON.stringify(report, null, 2));
|
|
await browser.close();
|
|
}
|
|
|
|
run().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|