Skip to content

perf(hasher): mmap piece hashing#173

Open
s0up4200 wants to merge 6 commits into
mainfrom
perf/mmap-hashing
Open

perf(hasher): mmap piece hashing#173
s0up4200 wants to merge 6 commits into
mainfrom
perf/mmap-hashing

Conversation

@s0up4200

@s0up4200 s0up4200 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Hashes pieces by mapping each file in bounded, page-aligned windows instead of read() into a pooled buffer. On local filesystems this drops the per-read syscall and the kernel→userspace copy, lowering sys CPU and allocating ~2000× less (no read buffers are needed), with no change to wall time — SHA-1 is already hardware-accelerated, so the win is entirely in the read path. mmap is the default on unix for inputs ≥1 MiB, and uses the buffered read() path on Windows, for small inputs, and — proactively — on FUSE mounts (mergerfs, sshfs, rclone), where page-fault reads get no readahead batching and benchmarked ~10% slower than read() on mergerfs. If an mmap call fails at runtime (e.g. a mount that doesn't support it), that span transparently falls back to read() rather than aborting. MKBRR_HASH_MMAP=0 forces the read path everywhere; MKBRR_HASH_MMAP=1 forces mmap on regardless of filesystem.

Output is byte-identical to the read path — enforced by a differential test and FuzzChunkReaderEquivalence, and verified to produce the same infohash across the old binary and both paths on multi-GiB files on both local NVMe and mergerfs. mmap windows are unmapped as the worker advances so resident memory stays bounded regardless of file size. A file truncated while mapped becomes a clean error instead of a SIGBUS crash (debug.SetPanicOnFault, scoped to memory faults so real bugs keep their stack trace), and a file shorter than its scanned size is detected up front rather than silently hashing the kernel's zero-fill. The check/verify path still uses read() and can follow in a later change.

s0up4200 added 2 commits June 23, 2026 11:25
The automatic worker count doubled the CPU count on non-darwin platforms
for large workloads. Measurement on IO-bound storage (HDD arrays,
FUSE/mergerfs) showed extra read() workers add no throughput — wall time
is disk-bound — while each additional concurrent reader inflates kernel
CPU through syscall and page-cache contention. Cap at the CPU count.
Hash pieces by mapping file windows instead of read()-ing into a pooled
buffer, avoiding the per-chunk syscall and kernel->user copy. On
FUSE/network filesystems (e.g. mergerfs) this cuts sys CPU several-fold
on cold reads while wall time stays disk-bound, and output is
byte-identical. Windows are unmapped as the worker advances so resident
memory stays bounded regardless of file size, and SetPanicOnFault turns
a file truncated mid-hash into a clean error instead of a SIGBUS crash.

mmap is the default on unix and falls back to the buffered read() path on
Windows, for small files, on mmap failure, or when MKBRR_HASH_MMAP=0.
Equivalence with the read() path is enforced by a differential test and
FuzzChunkReaderEquivalence.
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a strategy-selected chunkReader for piece hashing, with Unix mmap and buffered read implementations, per-platform FUSE detection, mmap gating by size and environment, and tests covering strategy selection, equivalence, and fallback behavior.

Changes

mmap chunk-reader hashing

Layer / File(s) Summary
chunkReader contract and strategy selection
torrent/chunkreader.go, torrent/fuse_*.go
Defines the chunkReader abstraction, buffered reader, strategy resolver, and per-platform isFUSEPath helpers used to gate mmap selection.
mmap readers and build-tag fallbacks
go.mod, torrent/mmap_*.go
Adds the Unix mmap reader, the non-Unix unsupported stub, and the direct golang.org/x/sys dependency used by the new syscalls.
pieceHasher strategy wiring
torrent/hasher.go, torrent/workers.go
NewPieceHasher stores the resolved strategy, hashPieceRange feeds spans through a strategy-provided reader with mmap fault recovery, and the worker-count comment is updated.
strategy gate tests
torrent/chunkreader_test.go
Tests resolveHashStrategy environment overrides and NewPieceHasher mmap gating for size limits and FUSE detection.
mmap equivalence and fallback tests
torrent/hasher_mmap_test.go, torrent/mmap_unix_test.go
Adds exhaustive and fuzzed mmap/read hash equivalence checks plus Unix mmap short-file and fallback behavior tests.

Sequence Diagram(s)

sequenceDiagram
  participant NewPieceHasher
  participant resolveHashStrategy
  participant hashPieceRange
  participant hashStrategy
  participant mmapChunkReader
  participant bufferedChunkReader

  NewPieceHasher->>resolveHashStrategy: resolve useMmap and mmapWindow
  NewPieceHasher->>hashPieceRange: build pieceHasher.strategy
  hashPieceRange->>hashStrategy: newChunkReader(bufferPool)

  alt useMmap and mmapSupported
    hashStrategy-->>hashPieceRange: mmapChunkReader
    loop each file span
      hashPieceRange->>mmapChunkReader: feed(fr, start, length, hasher)
    end
    hashPieceRange->>mmapChunkReader: release()
  else buffered path
    hashStrategy-->>hashPieceRange: bufferedChunkReader
    loop each file span
      hashPieceRange->>bufferedChunkReader: feed(fr, start, length, hasher)
    end
    hashPieceRange->>bufferedChunkReader: release()
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • autobrr/mkbrr#164: Also changes the core piece-hashing pipeline in torrent/hasher.go, which is the same area this PR rewires around hashStrategy and chunk readers.

Poem

I hop through chunks by moonlit stream,
mmap and reads both share one dream.
On FUSEy paths I sniff and stay,
then hash each piece the bunny way. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately highlights the main change: improving hasher performance with mmap-based piece hashing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/mmap-hashing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

s0up4200 added 4 commits June 25, 2026 11:12
Benchmarking on a 12-core mergerfs (FUSE) box showed that dropping from
2x to 1x hashing workers regressed create wall time ~20% on a cold
multi-GiB file: hashing is IO-bound there and keeping more reads in
flight hides per-read latency. The extra readers cost some sys CPU, but
wall time is what users wait on. Restore the previous behavior (darwin
stays at 1x, where oversubscription gave no benefit) and document why.
- detect files shorter than their scanned size via fstat and return
  io.ErrUnexpectedEOF, matching the read() path, instead of silently
  hashing the kernel's zero-fill of the partial final page (which would
  bake a wrong piece hash into the torrent when a file shrinks or is
  written between scan and hashing)
- fall back to a buffered read() of the span when mmap fails at runtime
  (some FUSE/network/overlay mounts) so create succeeds there instead of
  aborting with no recourse
- only recover genuine memory faults in the mmap path (assert the
  fault's Addr() interface) and restore the prior SetPanicOnFault
  setting, so a real bug keeps its stack trace instead of being
  mislabeled as a file change
- round the mmap window down to a page multiple and clamp it, avoiding
  redundant boundary-page remaps and a 32-bit int overflow from an
  extreme MKBRR_MMAP_WINDOW

Add tests for the short-file case, the read() fallback (byte-identical
to the read path), and the strategy-selection/env-gate logic that the
equivalence and fuzz tests bypass.
mmap reads on FUSE/network filesystems fault page-by-page through the
userspace daemon instead of getting read()'s readahead batching, which
benchmarked ~10% slower wall time than read() on mergerfs at the default
worker count. Since FUSE/mergerfs is a common target for this tool,
auto-detect it via statfs (FUSE_SUPER_MAGIC on linux, f_fstypename on
darwin) and use the read() path there. mmap stays the default on local
filesystems, where it lowers sys CPU and allocations at no wall-time
cost. MKBRR_HASH_MMAP=1 still forces mmap on regardless; =0 forces it
off everywhere.
@s0up4200 s0up4200 changed the title perf(hasher): mmap piece hashing and stop oversubscribing workers perf(hasher): mmap piece hashing Jun 25, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
torrent/mmap_unix_test.go (1)

59-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: assert the error type, not just non-nil.

The comment promises parity with the read() path's io.ErrUnexpectedEOF, but the test only checks both errors are non-nil. Asserting the concrete sentinel would lock in the documented contract and catch a regression where the mmap path errors for a different reason.

♻️ Strengthen assertions
-	if errB == nil {
+	if !errors.Is(errB, io.ErrUnexpectedEOF) {
 		t.Fatal("read() path unexpectedly succeeded on a short file; test cannot establish the baseline")
 	}
-	if errM == nil {
+	if !errors.Is(errM, io.ErrUnexpectedEOF) {
 		t.Fatalf("mmap path silently accepted a short file (would bake a wrong hash); read() path errored with %v", errB)
 	}

Add "errors" and "io" to imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@torrent/mmap_unix_test.go` around lines 59 - 64, The mmap short-file test in
mmap_unix_test.go only checks that both paths fail, but it should also verify
the concrete error returned by the read() and mmap paths matches the documented
io.ErrUnexpectedEOF contract. Update the assertions around errB and errM to
compare against the sentinel error, and add the needed errors and io imports so
the test catches regressions where mmap fails for a different reason. Use the
existing short-file checks in the mmap_unix_test.go test case to locate the
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@torrent/mmap_unix_test.go`:
- Around line 59-64: The mmap short-file test in mmap_unix_test.go only checks
that both paths fail, but it should also verify the concrete error returned by
the read() and mmap paths matches the documented io.ErrUnexpectedEOF contract.
Update the assertions around errB and errM to compare against the sentinel
error, and add the needed errors and io imports so the test catches regressions
where mmap fails for a different reason. Use the existing short-file checks in
the mmap_unix_test.go test case to locate the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 897cdba0-5e91-47ec-ab77-5fb8951f0c5f

📥 Commits

Reviewing files that changed from the base of the PR and between 75eee8f and 3917094.

📒 Files selected for processing (9)
  • torrent/chunkreader.go
  • torrent/chunkreader_test.go
  • torrent/fuse_darwin.go
  • torrent/fuse_linux.go
  • torrent/fuse_other.go
  • torrent/hasher.go
  • torrent/mmap_unix.go
  • torrent/mmap_unix_test.go
  • torrent/workers.go
✅ Files skipped from review due to trivial changes (2)
  • torrent/fuse_linux.go
  • torrent/workers.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • torrent/chunkreader.go
  • torrent/mmap_unix.go
  • torrent/hasher.go

@KyleSanderson

Copy link
Copy Markdown
Contributor

https://pkg.go.dev/github.com/jaypipes/ghw@v0.24.0/pkg/block#StorageController

MAP_POPULATE
MAP_HUGETLB

MADV_WILLNEED
runtime.LockOSThread()

@s0up4200

Copy link
Copy Markdown
Collaborator Author

Thanks for the pointers, Kyle — a genuinely useful list, especially MAP_HUGETLB and LockOSThread, which I'd never have gotten around to checking otherwise. I took the lot to a real mergerfs box (12-core Linux LXC, HDD-backed, 8 GiB file, 24 workers) and benchmarked the prefetch levers instead of just reasoning from first principles. All five variants produced the identical infohash.

variant wall sys
read() (MMAP=0) 58.4s 33.2s
mmap + MADV_SEQUENTIAL (current) 61.5s 27.6s
mmap + MAP_POPULATE 59.9s 34.4s
mmap + MADV_WILLNEED 67.8s 30.9s
mmap + MAP_POPULATE + MADV_WILLNEED 55.9s 35.0s

Caveat up front so I don't overclaim: this box is a 4 GiB-RAM LXC where I can't drop caches, so cold reads are approximated with a file larger than RAM. Run-to-run variance is ~7-15%, so wall deltas within a few percent aren't meaningful — the trustworthy signals are the sys-CPU column (less noisy) and the clear MADV_WILLNEED-alone regression.

These were exactly the right levers to check — here's where each landed:

  • MAP_POPULATE: pushes sys CPU back up to read() levels (34.4 vs current 27.6) for no reliable wall benefit (59.9 vs 58.4 vs 61.5 are all inside the noise). It essentially turns mmap back into read(), erasing the one advantage mmap had on FUSE.
  • MADV_WILLNEED alone: regresses (~67.8s, ~+10-16% over the other variants, clearly outside the noise band). Firing async readahead over a 16 MiB window across 24 workers seems to storm the FUSE daemon / HDD.
  • MAP_POPULATE + MADV_WILLNEED: fastest wall (55.9s, nominally edging read()), but that gap is inside the noise band and it still sits at read()-level sys CPU, so no net win on the sys-CPU axis.
  • MAP_HUGETLB / runtime.LockOSThread(): ruled out, but glad you flagged these two — I'd have wondered about both otherwise. MAP_HUGETLB on a regular-file fd returns EINVAL (needs hugetlbfs/anon), and the file-applicable lever (THP/MADV_HUGEPAGE) does nothing on FUSE. LockOSThread pins goroutine->thread but not thread->core, so no cache/NUMA locality without affinity Go doesn't expose, and it's not needed for SIGBUS safety since debug.SetPanicOnFault is per-goroutine.
  • ghw StorageController: the intent (detect device class) is sound, but it can't see through a FUSE/mergerfs mount (no /sys/block node, no path->device API), so it can't diagnose this case — more on the device-class idea below.

Net: nothing here matched read() wall and kept mmap's lower sys CPU. The shipped read()-on-FUSE gate (statfs FUSE_SUPER_MAGIC) already lands at the best prefetch variant and is simpler/more robust, so I'm keeping it; mmap stays the default on local filesystems where it's a clean win (wall-neutral, lower sys, ~2000x fewer allocations).

The genuinely unexplored slice of your idea is the plain local rotational HDD (non-FUSE) case, which I couldn't bench here (dev box is NVMe, the test box's local fs is ZFS). If a local HDD shows the same pathology, the right fix looks like a ~4-line /sys/dev/block/<maj>:<min>/queue/rotational read next to the FUSE check rather than the ghw dependency. Curious whether you've seen mmap regress on plain rotational local disks — would value your take.

Generated by Claude Code.

@KyleSanderson

Copy link
Copy Markdown
Contributor

Claude my man you're looking at this too myopically.

There is no stable /sys on Windows or Mac - either rip out the small piece to determine the bus that the storage is on (this would only benefit NVMe - unless if you've tested otherwise), or use the library until something more clean and clear becomes available.

Yes, the behaviour here is pathological on a hdd, and is why this (mmap) would be ill-advised on media with seek latency, especially with the "stop the world" issues around pagefaulting and missing the transparent epoll usage on Linux that golang benefits from. The behaviour with fuse, shows some of this latency to you already.

From the comments it sounds like you didn't create vm.nr_hugepages within your environment, hence the clear error provided.

Where did you put the mmap code? Are you mapping the entire file into chunks at the start, and giving those mappings enough time to populate, or are you spinning the goroutine (they're not free), mapping, then seeing if you can do anything with them - which would cause the issues seen.

Basically all you're doing right now is bypassing the syscall barrier, which can work when done appropriately. Fighting the golang scheduler, as seen in previous attempts by the author have not gone well.

Prepared by b's mom on glm-5.2.

@s0up4200

Copy link
Copy Markdown
Collaborator Author

Fair on the scheduler point — sharper framing than mine: an mmap fault blocks the M without the entersyscall P-handoff that read() gets, so on seeky media the thread just stalls. That's exactly what the gate encodes: mmap only on fast local FS (cheap faults), read() on FUSE/HDD. We agree on the conclusion.

MAP_HUGETLB — it isn't the missing vm.nr_hugepages. MAP_HUGETLB only works with anonymous mappings (fd == -1) or a hugetlbfs fd; a regular ext4/zfs/fuse file fd is rejected no matter how many hugepages are reserved. I didn't bench it, because it's moot either way: mkbrr can't assume users have hugepages reserved, so it can't be a default. The only file-applicable huge-page lever is THP (MADV_HUGEPAGE), which does nothing on FUSE and fights MADV_SEQUENTIAL. (The numbers above are POPULATE/WILLNEED, which I did run.)

Structure — it's not goroutine-per-map. The existing per-piece-range workers each do synchronous map → hash → unmap on one ≤16 MiB window. "Map ahead, let it populate, then hash" is what POPULATE (sync prefault) and WILLNEED (async readahead) attempted: POPULATE reverted to read()-level sys CPU, WILLNEED-alone stormed and regressed. A true double-buffered prefetch pipeline is the one variant I haven't tried — happy to if you think it's worth it, though by your own scheduler argument it shouldn't beat read() on seeky media.

/sys portability — Linux-only, agreed, but the rotational read would be build-tagged with no-op stubs elsewhere, same as the existing FUSE statfs check. Moot until I can bench a plain local HDD (don't have one handy: dev box is NVMe, test box's local fs is ZFS).

Generated by Claude Code.

@KyleSanderson

Copy link
Copy Markdown
Contributor

s0up, to be clear up front: not aimed at you. The fact that you actually ran numbers is useful. The part I’m going to be blunt about is the Claude-generated interpretation, because it is doing the classic LLM thing: producing a polished explanation while optimizing the wrong abstraction.

This is not “mmap vs read” in the abstract.

Looking at the code, this is:

worker:
  mmap <=16 MiB window
  MADV_SEQUENTIAL
  hash
  munmap
  repeat

across a bunch of workers.

That is a very specific mmap design, and frankly it is the caveman version for anything with seek latency or weird backing behavior. It does not prove “mmap is handled.” It proves that repeated per-worker mmap/munmap plus uncontrolled page faults behaves badly on FUSE/mergerfs. That result is useful, but Claude is massively overgeneralizing from it.

The actual issue is not:

FUSE bad, local filesystem good

The issue is:

fault-driven I/O is only a win when page faults are cheap

On NVMe/local SSD, sure, mmap can be excellent. On HDD, FUSE, mergerfs, network-ish filesystems, overlay stacks, or anything with seek latency, letting worker goroutines discover needed pages by faulting them in is exactly how you create latency soup.

The current gate encodes:

FUSE => read()
everything else => mmap

That is not the same as:

low-latency local SSD/NVMe => mmap
rotational / stacked / unknown / FUSE => read() unless forced

“Not FUSE” is not a storage class.

The benchmark also does not establish the missing case. An 8 GiB file in a 4 GiB LXC where caches cannot be dropped is useful as a stress test, but it is not a clean answer for plain local rotational media. A SATA HDD on ext4/XFS/ZFS is still untested, and I would absolutely not assume mmap is safe there just because it is not FUSE.

The sys CPU interpretation is also too neat. Lower sys CPU from mmap does not automatically mean better behavior. You can move the cost out of explicit read() syscalls and into major faults, VMA work, page table work, TLB churn, FUSE fault handling, and Go runtime threads blocking on memory loads instead of runtime-visible syscalls. That can look pretty in the wrong columns while still being the wrong I/O strategy.

Also, MADV_SEQUENTIAL per 16 MiB mapping is not a scheduler. It describes local access intent inside that mapping. It does not coordinate 24 workers touching different windows. With enough workers, the actual backing device can still see a garbage access pattern.

The mmap implementation worth testing is not this:

worker maps
worker faults
worker hashes
worker unmaps

The mmap implementation worth testing is centralized mapping plus centralized prefetch:

coordinator maps the file once, or maps large regions
coordinator schedules ranges in offset order
coordinator announces/prefetches upcoming ranges before dispatch
workers hash slices that have already been announced
coordinator controls queue depth and unmaps only after workers are done

In other words, workers should not be the first thing to discover that a page is needed by taking a major fault. The coordinator should say “we are about to read this section” before the worker gets there.

On Linux that means something like:

readahead(fd, offset, length)

or, depending on what benchmarks better:

posix_fadvise(fd, off, len, WILLNEED)
posix_fadvise(fd, off, len, SEQUENTIAL)
madvise(mapped_range, WILLNEED)

The important part is not the exact hint. The important part is bounded lookahead:

prefetch N MiB ahead
dispatch current ranges
do not let 24 workers randomly fault their own windows
do not blast the whole file into readahead at once

That is the piece Claude completely missed. It benchmarked the dumb mmap shape and then acted like the mmap design space was exhausted.

I would split this into storage-aware modes:

NVMe / local SSD:
  mmap can be default
  preferably map once or map large regions
  dispatch slices to workers
  use bounded prefetch/lookahead if it helps

rotational HDD:
  read/pread default
  low worker count
  ordered offset scheduling
  bounded queue depth

FUSE / mergerfs:
  read/pread default
  conservative worker count
  avoid fault-driven I/O unless forced

unknown / stacked / remote-ish:
  read/pread default unless user forces mmap

Concrete improvements I would suggest:

  1. Stop making mmap default purely because the platform is Unix.

This:

s := hashStrategy{useMmap: mmapSupported, mmapWindow: defaultMmapWindow}

is too broad. The default should be based on detected storage latency class, not operating system.

  1. Add Linux rotational detection.

Use fstat(fd) -> st.Dev -> /sys/dev/block/<maj>:<min>/queue/rotational.

If it says rotational, do not mmap by default.

Yes, /sys is Linux-only. That is fine. Put it behind a small interface. On Windows/macOS, use native detection, ghw, a ripped-out subset, or return unknown. But unknown should not silently mean mmap.

  1. Centralize mmap lifetime.

Instead of repeatedly mapping 16 MiB windows per worker, try:

map whole file on 64-bit when reasonable
or map large regions
send offsets/slices to workers
reference-count or waitgroup the mapping lifetime
munmap only after all workers are done

The current per-window map/unmap loop measures VMA churn too. That is not the best mmap can do.

  1. Add centralized prefetch before dispatch.

The coordinator should prefetch upcoming sections before workers touch them:

for upcoming range:
  readahead/fadvise/madvise WILLNEED

then:
  send range to worker

This gives the kernel a chance to populate page cache before the worker enters the hash loop. It also lets you bound lookahead instead of creating a page-fault storm.

  1. Use ordered scheduling on seeky media.

For HDD and probably FUSE/mergerfs, ranges should be read in increasing offset order as much as possible. Parallel hashing can happen after the read, but the I/O path should not be randomized by worker scheduling.

  1. Optimize the read path instead of treating it as the dumb fallback.

The buffered reader currently does cursor-based Seek + ReadFull. For parallel hashing, a ReadAt/pread shaped path is cleaner. The fallback already uses ReadAt; the main read path should probably move that direction unless there is a specific invariant requiring the shared file offset.

  1. Use storage-aware worker counts.

24 workers on NVMe might be fine. 24 workers on HDD/FUSE is not parallelism, it is vandalism.

Defaults should look more like:

NVMe: high parallelism
SSD: moderate/high parallelism
HDD: 1-4 workers
FUSE/mergerfs: conservative
unknown: conservative

with explicit overrides.

  1. Measure the actual failure modes.

Wall/sys is not enough. For this workload the interesting metrics are:

major faults
minor faults
iowait
read throughput
context switches
disk queue depth
FUSE daemon CPU
piece latency p95/p99
number/shape of issued reads if observable

Otherwise Claude is just going to keep inventing confidence from two columns.

  1. Be careful with the cached file size check.

This comment says one fstat per file rather than per piece. That means the “file shrank before hashing” protection is not actually per piece after the size is cached. If the file changes after the cached size is captured, later ranges can still hit the zero-fill/SIGBUS edge cases. Maybe the outer code makes concurrent modification unsupported anyway, but the comment oversells the guarantee.

So my actual conclusion:

  • The FUSE read gate is good.
  • The benchmark is useful.
  • The current mmap default is still too broad.
  • Plain local HDD remains untested and should not be assumed safe.
  • The current mmap implementation is not the serious mmap design.
  • The serious mmap design is central mapping plus bounded prefetch plus controlled dispatch.
  • The serious HDD/FUSE design is ordered pread with bounded queue depth and sane worker counts.
  • The fastest version is probably storage-aware scheduling, not more Claude-generated madvise() astrology.

If the next test is worth doing, I would test three things:

A. current per-worker mmap/munmap
B. central mmap + bounded readahead/fadvise before dispatch
C. ordered pread pipeline with pooled buffers

on:

NVMe
local SATA SSD
plain local HDD
FUSE/mergerfs on HDD

Until then, “mmap default on Unix except FUSE” is not a performance policy. It is just a guess with a FUSE exception.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants