// 文件操作基准(不进 ci):跨卷 move 1 GB、全量哈希 1 GB、10 万文件扫描;输出耗时与 MB/s。 // 用法:deno run -A scripts/bench/fs-bench.ts --base /opt/tmp-bench --other /dev/shm [--size-mb 1024] [--files 100000] // base:被测盘(源文件、扫描树所在);other:跨卷 move 的目标(必须与 base 不同设备) // 结果写进 docs/porting/moviepilot-rust-transfer.md「基准记录」。 import { randomBytes } from "node:crypto"; import { mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { parseArgs } from "node:util"; import { crossDeviceMove, FsProxy, resetSha256Probe, scanIncremental, sha256File, sha256Full, writeRootSentinel, } from "../../packages/adapters/src/index.ts"; const { values } = parseArgs({ args: Deno.args, options: { base: { type: "string" }, other: { type: "string", default: "/dev/shm" }, "size-mb": { type: "string", default: "1024" }, files: { type: "string", default: "100000" }, }, }); if (!values.base) { console.error("需要 --base <被测目录>"); Deno.exit(2); } const MB = 1024 * 1024; const sizeMb = Number(values["size-mb"]); const nFiles = Number(values.files); const work = await mkdtemp(join(values.base, "umt-bench-")); const other = await mkdtemp(join(values.other!, "umt-bench-x-")); if ((await stat(work)).dev === (await stat(other)).dev) console.warn("警告:base 与 other 在同一设备,move 不跨卷"); const rows: { case: string; ms: number; mbps: number | null; note?: string }[] = []; async function timed(name: string, bytes: number | null, fn: () => Promise, note?: string) { const t0 = performance.now(); await fn(); const ms = performance.now() - t0; rows.push({ case: name, ms: Math.round(ms), mbps: bytes ? Math.round(bytes / MB / (ms / 1000)) : null, ...(note ? { note } : {}), }); } async function makeBig(p: string) { const f = await Deno.open(p, { write: true, create: true, truncate: true }); const chunk = randomBytes(8 * MB); for (let i = 0; i < sizeMb / 8; i++) await f.write(chunk); f.close(); } const bytes = sizeMb * MB; const big = join(work, "big.bin"); try { await makeBig(big); // 冷缓存不可控(tmpfs 恒热;nvme 上刚写完也在页缓存里),数字代表热缓存吞吐 resetSha256Probe(); await timed("hash sha256sum 子进程", bytes, () => sha256Full(big)); await timed("hash 进程内流式", bytes, () => sha256File(big)); const px = new FsProxy({ mode: "subprocess" }); await timed("hash fsproxy 子进程(流式+心跳)", bytes, () => px.sha256(big)); for (const hash of ["size", "sha256"] as const) { const src = join(work, `mv-${hash}.bin`); await makeBig(src); await timed(`跨卷 move 进程内 hash=${hash}`, bytes, () => crossDeviceMove(src, join(other, `mv-${hash}.bin`), { hash }), ); const src2 = join(work, `mvp-${hash}.bin`); await makeBig(src2); let beats = 0; await timed(`跨卷 move fsproxy hash=${hash}`, bytes, () => px.crossDeviceMove(src2, join(other, `mvp-${hash}.bin`), { hash }, { onProgress: () => beats++ }), ); rows.at(-1)!.note = `心跳 ${beats} 次`; } const cp = join(work, "cp.bin"); await makeBig(cp); await timed("对照:cp 跨卷", bytes, async () => { const r = await new Deno.Command("cp", { args: [cp, join(other, "cp.bin")] }).output(); if (!r.success) throw new Error("cp failed"); }); // 10 万文件扫描:100 个目录 × 1000 个小文件 const tree = join(work, "scan"); await mkdir(tree); await writeRootSentinel(tree); const perDir = 1000; const t0 = performance.now(); for (let d = 0; d < Math.ceil(nFiles / perDir); d++) { const dir = join(tree, `d${String(d).padStart(3, "0")}`); await mkdir(dir); await Promise.all( Array.from({ length: Math.min(perDir, nFiles - d * perDir) }, (_, i) => writeFile(join(dir, `f${i}.mkv`), "x")), ); } const mkMs = Math.round(performance.now() - t0); let entries = 0; let batches = 0; await timed(`扫描 ${nFiles} 文件(scanIncremental,maxStats=2000 分批)`, null, async () => { let cursor: string | null = null; do { const r = await scanIncremental(tree, { cursor, maxStats: 2000 }); entries += r.entries.length; batches++; cursor = r.cursor; } while (cursor); }); rows.at(-1)!.note = `条目 ${entries},批 ${batches},建树 ${mkMs} ms`; await timed(`扫描 ${nFiles} 文件(单批)`, null, () => scanIncremental(tree, { maxStats: nFiles + 10 })); } finally { await rm(work, { recursive: true, force: true }); await rm(other, { recursive: true, force: true }); } console.log(`base=${values.base} other=${values.other} size=${sizeMb} MiB files=${nFiles}`); console.table(rows); console.log(JSON.stringify(rows));