OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay / bench / tree-bench.js

204 lines · javascript ref:
/**
 * Tree layer benchmarks — P.10 Phase 0.5a gate
 *
 * Run: bare bench/tree-bench.js [target-dir]
 *
 * Without an argument, defaults to the hyper-overlay repo itself. A real
 * scale run points this at a large node_modules (e.g. pear-git-v3's).
 *
 * Gates for 0.5a completion:
 *   - walkTree on ~30k files (pear-git-v3-scale) < 10s
 *   - walkTree on a single module (~1k files) < 2s
 *   - Cross-run determinism: same dir walked twice produces same hash
 *   - Hyperbee: 5k tree puts < 3s, per-tree get < 10ms median
 */

import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import hrtime from 'bare-hrtime'
import Corestore from 'corestore'
import crypto from 'hypercore-crypto'

import { walkTree } from '../lib/tree.js'
import { FileIndex } from '../lib/file-index.js'

const DEFAULT_TARGET = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..')

function now () { return Number(hrtime.bigint()) / 1e6 } // ms

function fmt (ms) { return ms.toFixed(1) + 'ms' }

async function benchWalkTree (targetDir, label) {
  console.log('─'.repeat(60))
  console.log(`walkTree: ${label}`)
  console.log(`  target: ${targetDir}`)

  if (!fs.existsSync(targetDir)) {
    console.log('  SKIP: directory does not exist')
    return null
  }

  // Count files first for context
  let fileCount = 0
  let totalBytes = 0
  function count (dir) {
    const entries = fs.readdirSync(dir)
    for (const name of entries) {
      if (name === 'node_modules' && dir.endsWith('/hyper-overlay')) continue // avoid recursing our own deps in default case
      const p = path.join(dir, name)
      try {
        const stat = fs.lstatSync(p)
        if (stat.isDirectory()) count(p)
        else if (stat.isFile()) {
          fileCount++
          totalBytes += stat.size
        }
      } catch {}
    }
  }
  try { count(targetDir) } catch {}
  console.log(`  files: ${fileCount}, total bytes: ${(totalBytes / 1048576).toFixed(1)} MB`)

  // Walk 1
  let t0 = now()
  const trees1 = walkTree(targetDir)
  const t1 = now() - t0
  console.log(`  walk 1: ${fmt(t1)}, ${trees1.size} trees`)

  // Walk 2 (determinism check)
  t0 = now()
  const trees2 = walkTree(targetDir)
  const t2 = now() - t0
  console.log(`  walk 2: ${fmt(t2)}, ${trees2.size} trees`)

  // Compare root hashes
  const root1 = trees1.get('').hash
  const root2 = trees2.get('').hash
  const deterministic = root1 === root2
  console.log(`  determinism: ${deterministic ? 'PASS' : 'FAIL'} (root hash matches)`)
  if (!deterministic) {
    console.log(`    walk1: ${root1}`)
    console.log(`    walk2: ${root2}`)
  }

  return { label, fileCount, totalBytes, walk1Ms: t1, walk2Ms: t2, deterministic, trees: trees1 }
}

async function benchHyperbee (trees) {
  console.log('─'.repeat(60))
  console.log('Hyperbee: bulk tree put + single-tree get')

  const storeDir = path.join(os.tmpdir(), 'tree-bench-' + crypto.randomBytes(4).toString('hex'))
  fs.mkdirSync(storeDir, { recursive: true })
  const store = new Corestore(storeDir)
  const fileIndex = new FileIndex(store)
  await fileIndex.ready()

  const entries = [...trees.entries()]
  const toInsert = entries.slice(0, Math.min(5000, entries.length))

  console.log(`  inserting ${toInsert.length} trees`)
  const t0 = now()
  for (const [relPath, tree] of toInsert) {
    await fileIndex.putTree(tree.hash, { entries: tree.entries, size: tree.size })
    await fileIndex.putTreePath(relPath || '/', tree.hash)
  }
  const insertMs = now() - t0
  console.log(`  put ${toInsert.length} trees: ${fmt(insertMs)} (${(insertMs / toInsert.length).toFixed(2)}ms/tree)`)

  // Get benchmark — 100 random lookups
  const sampleSize = Math.min(100, toInsert.length)
  const samples = []
  for (let i = 0; i < sampleSize; i++) {
    samples.push(toInsert[Math.floor(Math.random() * toInsert.length)])
  }

  const latencies = []
  for (const [, tree] of samples) {
    const ts = now()
    await fileIndex.getTree(tree.hash)
    latencies.push(now() - ts)
  }
  latencies.sort((a, b) => a - b)
  const median = latencies[Math.floor(latencies.length / 2)]
  const p95 = latencies[Math.floor(latencies.length * 0.95)]
  console.log(`  getTree: median ${fmt(median)}, p95 ${fmt(p95)} (over ${sampleSize} lookups)`)

  await fileIndex.close()
  await store.close()
  try { fs.rmSync(storeDir, { recursive: true, force: true }) } catch {}

  return { insertMs, insertCount: toInsert.length, median, p95 }
}

async function benchDedup (targetDirA, targetDirB) {
  console.log('─'.repeat(60))
  console.log('Dedup rate between two real module trees')
  console.log(`  A: ${targetDirA}`)
  console.log(`  B: ${targetDirB}`)

  if (!fs.existsSync(targetDirA) || !fs.existsSync(targetDirB)) {
    console.log('  SKIP: both dirs must exist')
    return null
  }

  const treesA = walkTree(targetDirA)
  const treesB = walkTree(targetDirB)

  const hashesA = new Set()
  const hashesB = new Set()
  for (const tree of treesA.values()) hashesA.add(tree.hash)
  for (const tree of treesB.values()) hashesB.add(tree.hash)

  const shared = new Set()
  for (const h of hashesA) if (hashesB.has(h)) shared.add(h)

  const uniqueA = hashesA.size
  const uniqueB = hashesB.size
  const totalUnique = new Set([...hashesA, ...hashesB]).size
  const dedupRatio = shared.size / Math.min(uniqueA, uniqueB)

  console.log(`  unique trees in A: ${uniqueA}`)
  console.log(`  unique trees in B: ${uniqueB}`)
  console.log(`  shared (identical hash): ${shared.size}`)
  console.log(`  total unique across both: ${totalUnique}`)
  console.log(`  dedup ratio (shared / min(A,B)): ${(dedupRatio * 100).toFixed(1)}%`)

  return { uniqueA, uniqueB, shared: shared.size, totalUnique, dedupRatio }
}

// ── Main ──────────────────────────────────────────────────────

async function main () {
  const target = Bare.argv[2] || DEFAULT_TARGET
  console.log(`\nTREE LAYER BENCHMARK — P.10 Phase 0.5a gate\n`)

  const r1 = await benchWalkTree(target, 'primary target')
  if (!r1) return

  await benchHyperbee(r1.trees)

  // Optional secondary dir for dedup bench
  const secondary = Bare.argv[3]
  if (secondary) {
    await benchDedup(target, secondary)
  }

  // Gate report
  console.log('─'.repeat(60))
  console.log('GATE CHECK')
  const walkGate = r1.fileCount > 20000 ? 10000 : 2000
  const walkPass = r1.walk1Ms < walkGate
  console.log(`  walkTree: ${fmt(r1.walk1Ms)} < ${walkGate}ms → ${walkPass ? 'PASS' : 'FAIL'}`)
  console.log(`  determinism: ${r1.deterministic ? 'PASS' : 'FAIL'}`)
  console.log('')
  console.log('  (Hyperbee + dedup gates are informational — see numbers above)')
  console.log('')
}

main().catch((err) => {
  console.error('bench failed:', err)
  Bare.exit(1)
})