mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-06 23:32:08 +00:00
92 lines
6.1 KiB
JavaScript
92 lines
6.1 KiB
JavaScript
// Ouverture d'un arbre dans un navigateur headless (CDP, sans dépendance) : temps jusqu'à l'arbre utilisable,
|
||
// jusqu'à la barre des onglets, et fonctions les plus coûteuses pendant le chargement.
|
||
// Usage : [TREE_PASSWORD='...'] node load-probe.mjs <url> [nom] [répétitions]
|
||
// url : un arbre sur un front local, de préférence un build (`vite build --minify false` puis `vite preview` :
|
||
// le mode dev fausse les temps, la minification les noms du profil)
|
||
// CPU=4 ralentit le processeur comme un téléphone moyen ; W et H fixent la fenêtre (1600 × 900).
|
||
// L'arbre est utilisable quand deux images passent après son chargement : le placement bloque le fil principal.
|
||
// Avec ?fixture= (chargeur local, jamais commité), on attend window.__pzFixtureLoaded posé par ce chargeur.
|
||
import { spawn } from 'node:child_process'
|
||
import { mkdtempSync, rmSync } from 'node:fs'
|
||
import { tmpdir } from 'node:os'
|
||
import path from 'node:path'
|
||
|
||
const URL_ = process.argv[2]
|
||
const NAME = process.argv[3] || 'arbre'
|
||
const RUNS = Number(process.argv[4] || 1)
|
||
const PASSWORD = process.env.TREE_PASSWORD
|
||
const BRAVE = process.env.BRAVE_PATH || 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe'
|
||
const CPU = Number(process.env.CPU || 1)
|
||
const W = Number(process.env.W || 1600), H = Number(process.env.H || 900)
|
||
if (!URL_) { console.error('usage : node load-probe.mjs <url> [nom] [répétitions]'); process.exit(1) }
|
||
const FIXTURE = new URL(URL_).searchParams.has('fixture')
|
||
|
||
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
|
||
class CDP {
|
||
constructor(ws) { this.ws = ws; this.id = 0; this.pending = new Map()
|
||
ws.onmessage = (m) => { const msg = JSON.parse(m.data); if (msg.id) { const p = this.pending.get(msg.id); this.pending.delete(msg.id); msg.error ? p.reject(new Error(msg.error.message)) : p.resolve(msg.result) } } }
|
||
send(method, params = {}) { const id = ++this.id; this.ws.send(JSON.stringify({ id, method, params })); return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject })) }
|
||
async eval(expression, awaitPromise = true) { const r = await this.send('Runtime.evaluate', { expression, awaitPromise, returnByValue: true }); if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || 'eval'); return r.result.value }
|
||
}
|
||
|
||
function topFunctions(profile, n) {
|
||
const byNode = new Map(profile.nodes.map(x => [x.id, x])); const self = new Map()
|
||
profile.samples.forEach((id, i) => { const cf = byNode.get(id).callFrame
|
||
const key = `${cf.functionName || '(anonyme)'} ${cf.url.split('/').slice(-1)[0].split('?')[0]}:${cf.lineNumber + 1}`
|
||
self.set(key, (self.get(key) || 0) + (profile.timeDeltas[i] || 0)) })
|
||
return [...self.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([fn, us]) => ({ fn, ms: Math.round(us / 1000) }))
|
||
}
|
||
|
||
async function once() {
|
||
const port = 9333 + Math.floor(Math.random() * 400)
|
||
const profileDir = mkdtempSync(path.join(tmpdir(), 'parenthese-load-'))
|
||
const browser = spawn(BRAVE, ['--headless=new', `--remote-debugging-port=${port}`, `--user-data-dir=${profileDir}`, `--window-size=${W},${H}`,
|
||
'--no-first-run', '--disable-extensions', '--disable-features=Translate,BraveRewards,BraveAds', 'about:blank'], { stdio: 'ignore' })
|
||
try {
|
||
let version
|
||
for (let i = 0; i < 100 && !version; i++) { try { const r = await fetch(`http://127.0.0.1:${port}/json/version`); if (r.ok) version = await r.json() } catch {} if (!version) await sleep(200) }
|
||
const bws = new WebSocket(version.webSocketDebuggerUrl); await new Promise(r => bws.onopen = r)
|
||
const { targetId } = await new CDP(bws).send('Target.createTarget', { url: 'about:blank' })
|
||
const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()
|
||
const pws = new WebSocket(list.find(t => t.id === targetId).webSocketDebuggerUrl); await new Promise(r => pws.onopen = r)
|
||
const p = new CDP(pws)
|
||
await p.send('Page.enable'); await p.send('Runtime.enable')
|
||
await p.send('Emulation.setDeviceMetricsOverride', { width: W, height: H, deviceScaleFactor: 1, mobile: false })
|
||
if (CPU > 1) await p.send('Emulation.setCPUThrottlingRate', { rate: CPU })
|
||
await p.send('Profiler.enable'); await p.send('Profiler.setSamplingInterval', { interval: 250 })
|
||
await p.send('Profiler.start')
|
||
const t0 = Date.now()
|
||
await p.send('Page.navigate', { url: URL_ })
|
||
if (PASSWORD) {
|
||
for (let i = 0; i < 100; i++) { if (await p.eval(`!!document.querySelector('input[type=password]')`, false).catch(() => false)) break; await sleep(100) }
|
||
await p.eval(`(() => {
|
||
const input = document.querySelector('input[type=password]')
|
||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(input, ${JSON.stringify(PASSWORD)})
|
||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||
;[...document.querySelectorAll('button')].find(b => /ouvrir/i.test(b.textContent))?.click()
|
||
})()`, false)
|
||
}
|
||
const loaded = FIXTURE ? `window.__pzFixtureLoaded === true` : `!!document.querySelector('canvas.galaxy-canvas')`
|
||
for (let i = 0; i < 600; i++) { if (await p.eval(loaded, false).catch(() => false)) break; await sleep(50) }
|
||
await p.eval(`new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))`)
|
||
const tree = Date.now() - t0
|
||
let tabs = null
|
||
for (let i = 0; i < 600; i++) { if (await p.eval(`document.querySelectorAll('.family-tab').length > 1`, false).catch(() => false)) { tabs = Date.now() - t0; break } await sleep(50) }
|
||
const { profile } = await p.send('Profiler.stop')
|
||
return { tree, tabs, top: topFunctions(profile, 10) }
|
||
} finally {
|
||
try { browser.kill() } catch {}
|
||
try { rmSync(profileDir, { recursive: true, force: true }) } catch {}
|
||
}
|
||
}
|
||
|
||
const runs = []
|
||
for (let i = 0; i < RUNS; i++) runs.push(await once())
|
||
const median = (xs) => { const s = xs.filter(x => x != null).sort((a, b) => a - b); return s.length ? s[Math.floor(s.length / 2)] : null }
|
||
console.log(JSON.stringify({
|
||
name: NAME, url: URL_, cpu: CPU, runs: RUNS,
|
||
tree_ms: median(runs.map(r => r.tree)), tabs_ms: median(runs.map(r => r.tabs)),
|
||
each: runs.map(r => [r.tree, r.tabs]), cpu_top_first_run: runs[0].top,
|
||
}, null, 1))
|
||
process.exit(0)
|