~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
208 lines · 8.2 KB Raw History
// 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()