#!/usr/bin/env bun /** * Glob benchmark: fff.glob vs Bun.Glob vs npm `glob`. * * Each engine is asked to enumerate files in a directory matching the same * pattern. We measure wall-clock time + result count. fff scans + indexes * once on init; the subsequent glob call is a filter over the in-memory * index — that's what we time. * * Usage: * bun examples/glob-bench.ts [dir] [pattern] [iterations] * * dir default: cwd * pattern default: "**\/*.ts" * iterations default: 5 (each engine runs N times, best+median reported) * * Install npm glob first: * bun add glob */ import { performance } from "node:perf_hooks"; import { resolve } from "node:path"; import { Glob as BunGlob } from "bun"; import { FileFinder } from "../src/index"; // npm glob — optional. Skip silently if not installed. let npmGlob: ((pattern: string, opts: { cwd: string }) => Promise) | null = null; try { const mod: { glob: (pattern: string, opts: { cwd: string }) => Promise; } = // @ts-ignore - optional peer; resolved at runtime, may be absent await import("glob"); npmGlob = mod.glob; } catch { console.warn("npm `glob` not installed — skipping. Run: bun add glob"); } const dir = resolve(process.argv[2] ?? process.cwd()); const pattern = process.argv[3] ?? "**/lua/**/*.lua"; const iterations = Number(process.argv[4] ?? 5); console.log(`dir: ${dir}`); console.log(`pattern: ${pattern}`); console.log(`iterations: ${iterations}\n`); interface Sample { ms: number; count: number; } function summarize(label: string, samples: Sample[]): void { if (samples.length === 0) { console.log(`${label.padEnd(16)} skipped`); return; } const sorted = [...samples].sort((a, b) => a.ms - b.ms); const best = sorted[0]!; const median = sorted[Math.floor(sorted.length / 2)]!; const worst = sorted[sorted.length - 1]!; const counts = new Set(samples.map((s) => s.count)); const countStr = counts.size === 1 ? `${best.count}` : `[${[...counts].join(", ")}]`; console.log( `${label.padEnd(16)} best=${best.ms.toFixed(2)}ms median=${median.ms.toFixed(2)}ms worst=${worst.ms.toFixed(2)}ms count=${countStr}`, ); } async function bench(fn: () => Promise | T): Promise<{ ms: number; result: T }> { const start = performance.now(); const result = await fn(); return { ms: performance.now() - start, result }; } // --------------------------------------------------------------------------- // fff: init + warm scan, then time only the .glob() call. Init cost is // reported separately because it's amortized across many subsequent calls. // --------------------------------------------------------------------------- const fffInit = await bench(() => { const result = FileFinder.create({ basePath: dir, disableMmapCache: true, disableContentIndexing: true, disableWatch: true, }); if (!result.ok) throw new Error(result.error); return result.value; }); const finder = fffInit.result; // Wait until initial scan done so the first .glob() doesn't see a partial // index. Returns true = completed, false = timed out. const scanReady = finder.waitForScanBlocking(30_000); if (!scanReady.ok || !scanReady.value) { console.error("fff: initial scan did not finish in 30s — exiting"); process.exit(1); } console.log(`fff init+scan: ${fffInit.ms.toFixed(2)}ms\n`); const fffSamples: Sample[] = []; for (let i = 0; i < iterations; i++) { const r = await bench(() => { const out = finder.glob(pattern, { pageSize: 100 }); if (!out.ok) throw new Error(out.error); return out.value; }); fffSamples.push({ ms: r.ms, count: r.result.items.length }); } // --------------------------------------------------------------------------- // Bun.Glob — sync iterator, returns relative paths. // --------------------------------------------------------------------------- const bunSamples: Sample[] = []; for (let i = 0; i < iterations; i++) { const r = await bench(() => { const g = new BunGlob(pattern); let count = 0; for (const _ of g.scanSync({ cwd: dir })) count++; return count; }); bunSamples.push({ ms: r.ms, count: r.result }); } // --------------------------------------------------------------------------- // npm glob — async, returns absolute or relative paths depending on opts. // --------------------------------------------------------------------------- const npmSamples: Sample[] = []; if (npmGlob) { for (let i = 0; i < iterations; i++) { const r = await bench(() => npmGlob!(pattern, { cwd: dir })); npmSamples.push({ ms: r.ms, count: r.result.length }); } } console.log("results:"); summarize("fff.glob", fffSamples); summarize("Bun.Glob", bunSamples); summarize("npm glob", npmSamples); // Sanity: counts should be in the same ballpark. They won't match exactly // because indexing rules differ (fff respects gitignore + skips binaries by // default; Bun.Glob and npm glob do not). const counts = { fff: fffSamples[0]?.count ?? 0, bun: bunSamples[0]?.count ?? 0, npm: npmSamples[0]?.count ?? 0, }; console.log( `\nNote: fff respects gitignore + skips binaries; Bun.Glob and npm glob walk the raw filesystem. Count differences are expected.`, ); console.log(`raw counts: fff=${counts.fff} bun=${counts.bun} npm=${counts.npm}`); finder.destroy();