mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 01:50:47 -04:00
performance slopping but actually very informative
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
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;
|
||||
});
|
||||
Reference in New Issue
Block a user