/**
* 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)
})