// Exercise native WebView creation with actual site pages. Start the app's release build with // WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port= and, preferably, // WEBVIEW2_USER_DATA_FOLDER pointing to a fresh directory. Nothing is clicked on the site. // Usage: SITE_PAGES=https://x.com/home,https://x.com/explore node dev-tools/native-webview-stress.mjs [6,12,24] [app-pid] // (the first page is opened in every tab; with the mixed route mode the second in every other tab) import { writeFile, unlink } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; const port = process.argv[2] ?? '9344'; const stages = (process.argv[3] ?? '6,12,24').split(',').map(Number); const appPid = process.argv[4] ? Number(process.argv[4]) : null; const memoryLimitMB = Number(process.env.WAYBETTER_STRESS_MEMORY_LIMIT_MB ?? 6144); const sitePages = (process.env.SITE_PAGES ?? '').split(',').map(page => page.trim()).filter(Boolean); if (!sitePages.length) throw new Error('SITE_PAGES is required (pages of the site to open, comma separated)'); const siteOrigin = new URL(sitePages[0]).origin; const routeMode = process.env.WAYBETTER_STRESS_ROUTE_MODE ?? 'home'; if (!stages.length || stages.some((n, i) => !Number.isInteger(n) || n < 1 || n > 24 || (i && n <= stages[i - 1]))) { throw new Error('Stages must increase from 1 to at most 24'); } if (appPid !== null && (!Number.isInteger(appPid) || appPid < 1)) throw new Error('Invalid app PID'); if (!Number.isFinite(memoryLimitMB) || memoryLimitMB < 1) throw new Error('Invalid memory limit'); if (!['home', 'mixed-home-explore'].includes(routeMode)) throw new Error('Invalid route mode'); const run = promisify(execFile); async function processSnapshot() { if (!appPid) return null; const command = `$all=Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId;$ids=@(${appPid});do{$n=@($all | Where-Object { $ids -contains $_.ParentProcessId -and $ids -notcontains $_.ProcessId } | Select-Object -ExpandProperty ProcessId);$ids+= $n}while($n.Count -gt 0);@(Get-Process -Id $ids -ErrorAction SilentlyContinue | Select-Object Id,ProcessName,CPU,WorkingSet64) | ConvertTo-Json -Compress`; const { stdout } = await run('powershell', ['-NoProfile', '-Command', command], { timeout: 15_000 }); const parsed = JSON.parse(stdout); return Array.isArray(parsed) ? parsed : [parsed]; } function processDelta(before, after) { if (!before || !after) return null; const old = new Map(before.map(item => [item.Id, item.CPU ?? 0])); const rows = after.map(item => ({ pid: item.Id, name: item.ProcessName, cpuSeconds: Math.round(((item.CPU ?? 0) - (old.get(item.Id) ?? 0)) * 10) / 10, memoryMB: Math.round((item.WorkingSet64 ?? 0) / 1024 / 1024), })).sort((a, b) => b.cpuSeconds - a.cpuSeconds); return { totalCpuSeconds: Math.round(rows.reduce((sum, row) => sum + row.cpuSeconds, 0) * 10) / 10, totalMemoryMB: rows.reduce((sum, row) => sum + row.memoryMB, 0), top: rows.slice(0, 6), }; } const targets = async () => (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()); const target = (await targets()).find(item => item.type === 'page' && item.url.startsWith('http://tauri.localhost/')); if (!target) throw new Error('Shell CDP target not found'); const socket = new WebSocket(target.webSocketDebuggerUrl); const ready = new Promise(resolve => { socket.onopen = resolve; }); const pending = new Map(); let serial = 0; socket.onmessage = event => { const message = JSON.parse(event.data); if (message.id) { pending.get(message.id)?.(message); pending.delete(message.id); } }; await ready; const send = (method, params = {}) => new Promise(resolve => { const id = ++serial; pending.set(id, resolve); socket.send(JSON.stringify({ id, method, params })); }); const evaluate = async (expression, timeoutMs = 45_000) => { let timeout; const result = await Promise.race([ send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }), new Promise((_, reject) => { timeout = setTimeout(() => reject(new Error(`Shell unresponsive for ${timeoutMs / 1000} s`)), timeoutMs); }), ]).finally(() => clearTimeout(timeout)); if (result.result?.exceptionDetails) throw new Error(JSON.stringify(result.result.exceptionDetails).slice(0, 600)); return result.result?.result?.value; }; const backup = join(tmpdir(), `waybetter-layout-before-stress-${process.pid}.json`); let completed = false; try { const original = await evaluate(`(() => { if (!window.__TAURI__?.event?.emit) throw new Error('Tauri event API unavailable'); const liveIds = () => [...document.querySelectorAll('.dv-tabs-container .dv-tab .bt-tab[data-panel-id]')] .map(tab => tab.dataset.panelId); const originalIds = liveIds(); if (new Set(originalIds).size !== originalIds.length) throw new Error('Duplicate live panel IDs'); const saved = JSON.parse(localStorage.getItem('bt.shell.layout.v1') || 'null'); if (Object.keys(saved?.panels ?? {}).length !== originalIds.length) { throw new Error('Saved panel count does not match live tabs; wait for initial layout persistence'); } window.__webStress = { originalIds: new Set(originalIds), initialCount: originalIds.length, layout: localStorage.getItem('bt.shell.layout.v1'), delays: [], longTasks: [], expected: performance.now() + 50, }; const s = window.__webStress; s.observer = new PerformanceObserver(list => { for (const entry of list.getEntries()) s.longTasks.push(entry.duration); }); s.observer.observe({ entryTypes: ['longtask'] }); s.interval = setInterval(() => { const now = performance.now(); s.delays.push(Math.max(0, now - s.expected)); s.expected = now + 50; }, 50); return { initialCount: s.initialCount, layout: s.layout }; })()`); // This file is a recovery copy if the shell dies before its tabs can be closed. It is deleted // after a successful cleanup; its contents are never printed or committed. await writeFile(backup, JSON.stringify({ layout: original.layout }), { mode: 0o600 }); console.log(JSON.stringify({ initialTabs: original.initialCount, recoveryFile: backup })); let previous = 0; let stopReason = null; for (const count of stages) { const beforeProcesses = await processSnapshot(); const added = count - previous; const urls = Array.from({ length: added }, (_, index) => routeMode === 'mixed-home-explore' && (previous + index) % 2 === 1 ? sitePages[1] ?? sitePages[0] : sitePages[0]); const stage = await evaluate(`(async () => { const s = window.__webStress; const start = performance.now(); const calls = ${JSON.stringify(urls)}.map(url => window.__TAURI__.event.emit('tab-open', { url, background: true, })); await Promise.all(calls); const dispatchMs = performance.now() - start; const expected = s.initialCount + ${count}; const deadline = performance.now() + 30_000; while (document.querySelectorAll('.dv-tab').length < expected && performance.now() < deadline) { await new Promise(resolve => setTimeout(resolve, 100)); } await new Promise(resolve => setTimeout(resolve, 8_000)); const saved = JSON.parse(localStorage.getItem('bt.shell.layout.v1') || 'null'); const liveIds = [...document.querySelectorAll('.dv-tabs-container .dv-tab .bt-tab[data-panel-id]')] .map(tab => tab.dataset.panelId); const extraIds = liveIds.filter(id => !s.originalIds.has(id)); return { requested: ${count}, routeMode: ${JSON.stringify(routeMode)}, tabs: liveIds.length, liveExtraIds: extraIds.length, savedExtraIds: Object.keys(saved?.panels ?? {}).filter(id => !s.originalIds.has(id)).length, dispatchMs: Math.round(dispatchMs), maxDelayMs: Math.round(Math.max(0, ...s.delays)), maxLongTaskMs: Math.round(Math.max(0, ...s.longTasks)), }; })()`, 50_000); const list = await targets(); stage.xTargets = list.filter(item => item.type === 'page' && item.url.startsWith(`${siteOrigin}/`)).length; stage.processes = processDelta(beforeProcesses, await processSnapshot()); console.log(JSON.stringify(stage)); if (stage.tabs !== original.initialCount + count || stage.liveExtraIds !== count) { throw new Error('New live tab count or panel ID count did not reach target'); } previous = count; if (stage.maxDelayMs > 1500 || (stage.processes?.totalMemoryMB ?? 0) > memoryLimitMB) { stopReason = stage.maxDelayMs > 1500 ? 'shell timer lag exceeded 1.5 s' : `process working sets exceeded ${memoryLimitMB} MB`; break; } } if (stopReason) console.log(JSON.stringify({ stoppedAt: previous, reason: stopReason })); completed = previous === stages.at(-1); if (!completed) console.error(`Ramp incomplete: reached ${previous} of ${stages.at(-1)} requested site tabs`); } finally { try { const cleaned = await evaluate(`(async () => { const s = window.__webStress; if (!s) return { error: 'probe missing' }; clearInterval(s.interval); s.observer.disconnect(); await new Promise(resolve => setTimeout(resolve, 600)); const panelIds = () => [...document.querySelectorAll('.dv-tabs-container .dv-tab .bt-tab[data-panel-id]')] .map(tab => tab.dataset.panelId); const extras = panelIds().filter(id => !s.originalIds.has(id)); for (const id of extras) { // Explicit missing IDs are ignored. Confirm each live ID before sending, then wait for // that exact panel to disappear before closing the next one. if (!panelIds().includes(id)) continue; await window.__TAURI__.event.emit('tab-shortcut', { id, shortcut: 'close-tab' }); const deadline = performance.now() + 3000; while (panelIds().includes(id) && performance.now() < deadline) { await new Promise(resolve => setTimeout(resolve, 50)); } if (panelIds().includes(id)) throw new Error('Panel close did not persist: ' + id); } await new Promise(resolve => setTimeout(resolve, 500)); const remainingIds = panelIds(); if (s.layout === null) localStorage.removeItem('bt.shell.layout.v1'); else localStorage.setItem('bt.shell.layout.v1', s.layout); return { extraTabs: extras.length, remainingTabs: remainingIds.length, expectedTabs: s.initialCount, originalIdsRestored: remainingIds.length === s.originalIds.size && remainingIds.every(id => s.originalIds.has(id)), layoutRestored: localStorage.getItem('bt.shell.layout.v1') === s.layout }; })()`, 90_000); console.log(JSON.stringify({ cleanup: cleaned })); if (cleaned.remainingTabs !== cleaned.expectedTabs || !cleaned.originalIdsRestored || !cleaned.layoutRestored) { completed = false; } else { await unlink(backup); } } catch (error) { completed = false; console.error(`Cleanup failed: ${error.message}; recovery layout is at ${backup}`); } socket.close(); if (!completed) process.exitCode = 1; }