waybetter-for-x/apps/desktop/scripts/perf-probe.mjs
2026-10-01 06:01:13 +07:00

139 lines
9.5 KiB
JavaScript

// Performance probe for what wayBetter injects into X tabs. Loads tab-bridge.js, observer.js and
// x-runtime.js as init scripts (like the native tab webview) into headless Chromium and measures:
// script evaluation time, fetch/XHR wrapper layers and their synchronous overhead, Response clones
// and large JSON.parse calls made by our code, MutationObserver callbacks / rAF / timer work
// attributed to our scripts, and main-thread long tasks. The probe is the framework's
// (waybetter/dev-tools/perf-probe.mjs); X's scripts, fixtures and scenarios are here.
//
// node scripts/perf-probe.mjs [--dir <folder with the three scripts> | --none] [--seconds 8] [--runs 3] [--live]
//
// Fixture scenarios intercept every request locally (an X-like timeline at /home and a chat page at
// /i/chat/…, both with synthetic traffic). `--live` additionally opens a logged-out x.com profile
// page over the network (read-only, no interaction besides scrolling). Numbers are headless Chromium,
// not WebView2; compare runs on the same machine only.
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { chromium } from 'playwright';
import { runPerfProbe } from '../../../waybetter/dev-tools/perf-probe.mjs';
const args = process.argv.slice(2);
const flag = name => args.includes(name);
const option = (name, fallback) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : fallback; };
const dir = option('--dir', path.resolve(import.meta.dirname, '../src-tauri'));
const none = flag('--none');
const seconds = Number(option('--seconds', '8'));
const live = flag('--live');
const liveUrl = option('--url', 'https://x.com/NASA');
const runs = Number(option('--runs', '3'));
const scripts = none ? [] : await Promise.all(['tab-bridge', 'observer', 'x-runtime'].map(async name => ({
// The tab bridge is the framework's (waybetter-host); the other two are this app's build outputs.
name, code: await readFile(name === 'tab-bridge' ? path.resolve(import.meta.dirname, '../../../waybetter/crates/waybetter-host/tab-bridge.js') : path.join(dir, `${name}.js`), 'utf8'),
})));
// ---------------------------------------------------------------------------------------- fixtures
const NAV = `<header role="banner"><h1><a href="/home"><svg viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg></a></h1>
<nav role="navigation"><a data-testid="AppTabBar_Home_Link" href="/home"><div><svg><path d="M1"/></svg><span>Home</span></div></a>
<a data-testid="AppTabBar_Notifications_Link" href="/notifications"><div><svg><path d="M1"/></svg><div aria-live="polite">3</div><span>Notifications</span></div></a>
<a data-testid="AppTabBar_DirectMessage_Link" href="/i/chat"><div><svg><path d="M1"/></svg><span>Chat</span></div></a>
<button data-testid="AppTabBar_More_Menu" class="m1"><div class="p1"><svg width="26"><path d="M1"/></svg><span>More</span></div></button>
<a data-testid="SideNav_NewTweet_Button" href="/compose/post"><span><span>Post</span></span></a></nav></header>`;
const SIM_COMMON = `
const sleep = ms => new Promise(r => setTimeout(r, ms));
const traffic = () => {
fetch('https://abs.twimg.com/responsive-web/client-web/vendor.wasm').then(r => r.arrayBuffer()).catch(() => {});
fetch('https://pbs.twimg.com/media/Fabc.jpg?name=small').then(r => r.blob()).catch(() => {});
fetch('/i/api/1.1/jot/client_event.json', { method: 'POST', body: 'log=%5B%5D' }).catch(() => {});
const x = new XMLHttpRequest(); x.open('GET', '/i/api/2/badge_count/badge_count.json'); x.send();
};`;
const TIMELINE_SIM = `${SIM_COMMON}
let n = 1800000000000000000n;
const col = document.getElementById('tl');
function article() {
const id = String(n++);
const a = document.createElement('article');
a.setAttribute('data-testid', 'tweet');
a.innerHTML = '<div data-testid="UserAvatar-Container-jack"><img src="data:,"></div><div><a href="/jack">Jack</a> <a href="/jack/status/' + id + '"><time datetime="2026-09-27T10:00:00Z">1h</time></a></div>' +
'<div data-testid="tweetText"><span>Post number ' + id + ' with some text that goes on for a while</span></div>' +
'<div data-testid="tweetPhoto"><img src="data:,"></div>' +
'<div role="group"><button data-testid="reply" aria-label="12 Replies. Reply"><span>12</span></button><button data-testid="retweet" aria-label="1234 reposts. Repost"><span>1.2K</span></button>' +
'<button data-testid="like" aria-label="45678 Likes. Like"><span>45.6K</span></button><a href="/jack/status/' + id + '/analytics" aria-label="99999 views"><span>99K</span></a><button data-testid="bookmark"><span></span></button></div>';
return a;
}
for (let i = 0; i < 20; i++) col.appendChild(article());
setInterval(() => {
col.appendChild(article());
if (col.children.length > 25) col.firstElementChild.remove();
const spans = col.querySelectorAll('[data-testid="like"] span');
const s = spans[(Math.random() * spans.length) | 0]; if (s) s.firstChild.nodeValue = String((Math.random() * 99) | 0) + 'K';
window.scrollBy(0, 30);
}, 60);
setInterval(() => fetch('/i/api/graphql/abc123/HomeTimeline?variables=%7B%22count%22%3A20%7D&features=%7B%7D').then(r => r.json()).catch(() => {}), 400);
setInterval(traffic, 120);`;
const CHAT_SIM = `${SIM_COMMON}
const list = document.getElementById('msgs');
let m = 0;
setInterval(() => {
const d = document.createElement('div');
d.setAttribute('data-testid', 'messageEntry');
d.innerHTML = '<div><span>message ' + (m++) + ' from a friend</span><time>now</time></div>';
list.appendChild(d);
if (list.children.length > 60) list.firstElementChild.remove();
}, 50);
setInterval(() => fetch('/i/api/graphql/zz9/GetConversationPage?variables=%7B%7D').then(r => r.json()).catch(() => {}), 250);
setInterval(() => fetch('https://abs.twimg.com/responsive-web/client-web/chat-crypto.wasm').then(r => r.arrayBuffer()).catch(() => {}), 200);
setInterval(traffic, 120);`;
const timelineHtml = `<!doctype html><html lang="en"><head><title>Home / X</title><link rel="icon" href="https://abs.twimg.com/favicons/twitter.3.ico"></head><body style="margin:0;background:#000;color:#e7e9ea">
<div id="react-root" style="display:flex">${NAV}<main style="flex:1"><div data-testid="primaryColumn" style="width:600px">
<div role="tablist"><div role="tab" aria-selected="true">For you</div><div role="tab" aria-selected="false">Following</div></div>
<div><div data-testid="tweetTextarea_0" role="textbox" contenteditable="true"></div><div data-testid="toolBar"><div role="tablist"></div><button data-testid="tweetButtonInline">Post</button></div></div>
<section id="tl"></section></div><div data-testid="sidebarColumn"><input data-testid="SearchBox_Search_Input" type="text"></div></main></div>
<script>${TIMELINE_SIM}</script></body></html>`;
const chatHtml = `<!doctype html><html lang="en"><head><title>Chat / X</title></head><body style="margin:0;background:#000;color:#e7e9ea">
<div id="react-root" style="display:flex">${NAV}<main style="flex:1"><section id="msgs" style="height:600px;overflow:auto"></section>
<div data-testid="dmComposerTextInput" role="textbox" contenteditable="true"></div></main></div>
<script>${CHAT_SIM}</script></body></html>`;
function timelineJson() {
const entries = [];
for (let i = 0; i < 40; i++) {
const id = String(1900000000000000000n + BigInt(i));
entries.push({ entryId: `tweet-${id}`, content: { itemContent: { tweet_results: { result: {
__typename: 'Tweet', rest_id: id,
core: { user_results: { result: { rest_id: '12', legacy: { screen_name: 'jack', name: 'Jack', description: 'x'.repeat(200), url: null } } } },
legacy: { full_text: 'lorem ipsum '.repeat(40), extended_entities: { media: [{ type: 'photo', media_url_https: `https://pbs.twimg.com/media/A${i}.jpg` }] } },
} } } } });
}
const body = JSON.stringify({ data: { home: { home_timeline_urt: { instructions: [{ type: 'TimelineAddEntries', entries }] } } }, pad: 'p'.repeat(250_000) });
return body;
}
const TIMELINE_JSON = timelineJson();
const WASM = Buffer.alloc(2 * 1024 * 1024, 7);
async function routeAll(context) {
await context.route('**/*', route => {
const url = new URL(route.request().url());
if (url.hostname === 'abs.twimg.com' && url.pathname.endsWith('.wasm')) return route.fulfill({ status: 200, contentType: 'application/wasm', body: WASM });
if (url.hostname === 'abs.twimg.com') return route.fulfill({ status: 200, contentType: 'image/x-icon', body: Buffer.alloc(64) });
if (url.hostname === 'pbs.twimg.com') return route.fulfill({ status: 200, contentType: 'image/jpeg', body: Buffer.alloc(40_000, 1) });
if (url.origin !== 'https://x.com') return route.abort();
if (/\/HomeTimeline$/.test(url.pathname)) return route.fulfill({ status: 200, contentType: 'application/json', body: TIMELINE_JSON });
if (url.pathname.startsWith('/i/api/')) return route.fulfill({ status: 200, contentType: 'application/json', body: '{"ok":true,"data":{}}' });
return route.fulfill({ status: 200, contentType: 'text/html', body: url.pathname.startsWith('/i/chat') ? chatHtml : timelineHtml });
});
}
// x.com answers headless Chromium with 403 (bot protection); the live scenario only works where it is served.
const results = await runPerfProbe({
chromium, scripts, seconds, runs,
scenarios: [
{ name: 'fixture /home', url: 'https://x.com/home', route: routeAll },
{ name: 'fixture /i/chat', url: 'https://x.com/i/chat/123-456', route: routeAll },
...(live ? [{ name: `live ${liveUrl}`, url: liveUrl, scroll: true }] : []),
],
});
console.log(JSON.stringify({ build: none ? 'none' : dir, seconds, results }, null, 2));