johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
// How a pull's files page does in a browser: its first load, then a
// reader going through it to the end, a screen every 150 ms, as each
// file's rows are fetched and drawn. Where the time goes, from Chromium's
// own counts: script, layout and style, the long tasks that froze the
// page and the longest, the getPullDiff calls made, and how large the
// page grew.
//
// node harness/files-bench.mjs <files page url> [--runs <n>] [--throttle <x>] [--nohl] [--profile]
//
// Any page will do, a local one or knotless.dev's. `--runs` takes the
// median of several (3); `--throttle` slows the CPU that many times, as
// a slower laptop or a phone; `--nohl` turns the highlighting off, for
// the floor the rest is measured against; `--profile` instead profiles
// one pass and prints the functions that took longest, by their own
// time and with what they called, keeping the profile in
// target/files-bench.cpuprofile to open in Chrome's devtools.
import { chromium } from '@playwright/test'
import { mkdirSync, writeFileSync } from 'node:fs'
const args = process.argv.slice(2)
const url = args.find((a) => !a.startsWith('--') && /^https?:/.test(a))
if (!url) {
console.error(
'usage: node harness/files-bench.mjs <files page url> [--runs <n>] [--throttle <x>] [--nohl] [--profile]',
)
process.exit(2)
}
const flag = (name) => args.includes(name)
const option = (name, value) => (flag(name) ? Number(args[args.indexOf(name) + 1]) : value)
const runs = option('--runs', 3)
const throttle = option('--throttle', 1)
/** Chromium's counts, by name. */
async function counts(cdp) {
const { metrics } = await cdp.send('Performance.getMetrics')
return Object.fromEntries(metrics.map((m) => [m.name, m.value]))
}
/** The milliseconds counted as `name` between `a` and `b`. */
const spent = (a, b, name) => Math.round(((b[name] ?? 0) - (a[name] ?? 0)) * 1000)
/** A page open at `url`, its long tasks kept, and a wait for the getPullDiff calls under way. */
async function opened(browser) {
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } })
const cdp = await page.context().newCDPSession(page)
await cdp.send('Performance.enable')
if (throttle > 1) await cdp.send('Emulation.setCPUThrottlingRate', { rate: throttle })
await page.addInitScript(() => {
window.__long = []
const seen = (list) => list.getEntries().forEach((e) => window.__long.push(e.duration))
new PerformanceObserver(seen).observe({ type: 'longtask', buffered: true })
})
if (flag('--nohl'))
await page.addInitScript(() => Object.defineProperty(CSS, 'highlights', { value: undefined }))
const diffs = []
let going = 0
const isDiff = (r) => r.url().includes('dev.knotless.getPullDiff')
page.on('request', (r) => isDiff(r) && going++)
page.on('requestfailed', (r) => isDiff(r) && going--)
page.on('requestfinished', async (r) => {
if (!isDiff(r)) return
going--
const sizes = await r.sizes().catch(() => ({}))
diffs.push({ ms: Math.round(r.timing().responseEnd), bytes: sizes.responseBodySize ?? 0 })
})
// The page keeps a connection open for what changes, so it is never
// idle: what it waits on is its diffs.
const settled = async () => {
await page.waitForTimeout(300)
while (going > 0) await page.waitForTimeout(50)
await page.waitForTimeout(100)
}
return { page, cdp, diffs, settled }
}
/** Scrolls `page` a screen at a time to its end, waiting for what that fetches; the longest a step lagged. */
async function read(page, settled) {
const started = Date.now()
let lag = 0
for (;;) {
const step = Date.now()
const end = await page.evaluate(() => {
window.scrollBy(0, innerHeight)
return innerHeight + scrollY >= document.body.scrollHeight - 2
})
await page.waitForTimeout(150)
lag = Math.max(lag, Date.now() - step - 150)
if (end) {
await settled()
const still = await page.evaluate(
() => innerHeight + scrollY >= document.body.scrollHeight - 2,
)
if (still) break
}
if (Date.now() - started > 180_000) break
}
return lag
}
const longTasks = (page) =>
page.evaluate(() => ({
total: Math.round(window.__long.reduce((s, d) => s + d, 0)),
most: Math.round(Math.max(0, ...window.__long)),
}))
const nodes = (page) => page.evaluate(() => document.querySelectorAll('*').length)
async function once(browser) {
const { page, cdp, diffs, settled } = await opened(browser)
const before = await counts(cdp)
const response = await page.goto(url, { waitUntil: 'load' })
const html = (await response.body()).length
await settled()
const nav = await page.evaluate(() => {
const n = performance.getEntriesByType('navigation')[0]
const fcp = performance.getEntriesByName('first-contentful-paint')[0]
return {
ttfb: Math.round(n.responseStart),
fcp: Math.round(fcp?.startTime ?? 0),
load: Math.round(n.loadEventEnd),
}
})
const loaded = await counts(cdp)
const atLoad = await longTasks(page)
const nodesAtLoad = await nodes(page)
const files = await page.evaluate(
() => document.querySelectorAll('.pull-files-diffs > .anchor').length,
)
const started = Date.now()
const lag = await read(page, settled)
const scrolled = Date.now() - started
const done = await counts(cdp)
const atEnd = await longTasks(page)
const result = {
files,
html_kb: Math.round(html / 1024),
...nav,
load_script_ms: spent(before, loaded, 'ScriptDuration'),
load_layout_ms: spent(before, loaded, 'LayoutDuration'),
load_style_ms: spent(before, loaded, 'RecalcStyleDuration'),
load_long_ms: atLoad.total,
nodes_at_load: nodesAtLoad,
read_ms: scrolled,
read_script_ms: spent(loaded, done, 'ScriptDuration'),
read_layout_ms: spent(loaded, done, 'LayoutDuration'),
read_style_ms: spent(loaded, done, 'RecalcStyleDuration'),
read_long_ms: atEnd.total - atLoad.total,
longest_task_ms: atEnd.most,
worst_step_lag_ms: lag,
diffs: diffs.length,
diff_median_ms: diffs.map((d) => d.ms).sort((a, b) => a - b)[diffs.length >> 1] ?? 0,
diff_kb: Math.round(diffs.reduce((s, d) => s + d.bytes, 0) / 1024),
nodes_at_end: await nodes(page),
heap_mb: Math.round((done.JSHeapUsedSize ?? 0) / 1e6),
}
await page.close()
return result
}
/** One pass read under the profiler, and its slowest functions printed. */
async function profiled(browser) {
const { page, cdp, settled } = await opened(browser)
await page.goto(url, { waitUntil: 'load' })
await settled()
await cdp.send('Profiler.enable')
await cdp.send('Profiler.setSamplingInterval', { interval: 500 })
await cdp.send('Profiler.start')
await read(page, settled)
const { profile } = await cdp.send('Profiler.stop')
await page.close()
mkdirSync('target', { recursive: true })
writeFileSync('target/files-bench.cpuprofile', JSON.stringify(profile))
const byId = new Map(profile.nodes.map((n) => [n.id, n]))
const parent = new Map()
for (const n of profile.nodes) for (const c of n.children ?? []) parent.set(c, n.id)
const name = (id) => {
const f = byId.get(id).callFrame
return `${f.functionName || '(anonymous)'} ${f.url.split('/').pop()}:${f.lineNumber + 1}`
}
const own = new Map()
const all = new Map()
profile.samples.forEach((id, i) => {
const dt = profile.timeDeltas[i] ?? 0
own.set(name(id), (own.get(name(id)) ?? 0) + dt)
const seen = new Set()
for (let at = id; at; at = parent.get(at)) seen.add(name(at))
for (const n of seen) all.set(n, (all.get(n) ?? 0) + dt)
})
const top = (map, n) =>
[...map]
.sort((a, b) => b[1] - a[1])
.slice(0, n)
.map(([k, v]) => `${String(Math.round(v / 1000)).padStart(8)} ms ${k}`)
console.log(
`by own time:\n${top(own, 20).join('\n')}\n\nwith what each called:\n${top(all, 30).join('\n')}`,
)
console.log('\nkept in target/files-bench.cpuprofile')
}
const browser = await chromium.launch()
if (flag('--profile')) await profiled(browser)
else {
const results = []
for (let i = 0; i < runs; i++) results.push(await once(browser))
const median = (k) => results.map((r) => r[k]).sort((a, b) => a - b)[results.length >> 1]
const out = Object.fromEntries(Object.keys(results[0]).map((k) => [k, median(k)]))
console.log(JSON.stringify({ url, runs, throttle, nohl: flag('--nohl'), ...out }, null, 2))
}
await browser.close()