Skip to content

feat(fuse): async read-ahead buffer for smoother FUSE throughput#493

Merged
javi11 merged 1 commit intomainfrom
feat/fuse-async-read-buffer
Apr 12, 2026
Merged

feat(fuse): async read-ahead buffer for smoother FUSE throughput#493
javi11 merged 1 commit intomainfrom
feat/fuse-async-read-buffer

Conversation

@javi11
Copy link
Copy Markdown
Owner

@javi11 javi11 commented Apr 12, 2026

Summary

  • Add AsyncReadBuffer at the FUSE layer that wraps ReadAtContext with a background goroutine filling a ring buffer
  • FUSE reads now pull from pre-filled memory instead of blocking on NNTP segment downloads
  • Configurable via async_buffer_size in FUSE config (default 8MB, 0 to disable)
  • Frontend UI added for the new setting in the Streaming Engine section

Problem

FUSE reads blocked synchronously on NNTP segment downloads. Profiling showed 66.7% of transfer time was stalling with 193 gaps >100ms during a 2.3GB file transfer. During active reads throughput was 111 MB/s, but the pipeline stalled whenever reads caught up to the download frontier.

Solution

Inspired by rclone's AsyncReader (which provides smooth reads even without VFS cache), this adds an AsyncReadBuffer that:

  1. Spawns a background goroutine that continuously reads from the source into a ring buffer
  2. FUSE reads serve from pre-filled memory (microseconds) instead of blocking on network (milliseconds)
  3. Only activates for files larger than the buffer size (skips Finder/Spotlight metadata reads)
  4. Uses lazy initialization — no memory allocated until first read
  5. Lives at the FUSE level only — WebDAV path is unaffected

Results

Metric Before After
FUSE cat speed ~24 MB/s (94s) ~41 MB/s (55s)
rclone cat speed ~43 MB/s (53s) unchanged
Gap vs rclone 75% slower ~4% slower

Test plan

  • 8 unit tests with -race for AsyncReadBuffer (sequential reads, slow source, error propagation, concurrent read+close, passthrough, GetBufferedOffset)
  • go build ./... passes
  • Existing handle tests updated and passing
  • Manual: rebuild, restart, dd if=<fuse-file> of=/dev/null bs=1m — verify ~40+ MB/s
  • Manual: check pprof heap — verify ~8MB per active file stream
  • Manual: verify Finder browsing doesn't trigger buffer allocation for small files

FUSE reads were blocking synchronously on NNTP segment downloads, causing
66.7% of transfer time to be spent stalling (193 gaps >100ms in a 2.3GB
transfer). This was because each 16KB FUSE read directly blocked on
UsenetReader segment availability with no buffering layer.

Add AsyncReadBuffer that wraps ReadAtContext with a background goroutine
continuously filling a ring buffer. FUSE reads now pull from pre-filled
memory instead of blocking on network I/O, matching rclone's AsyncReader
pattern.

Key design decisions:
- Buffer lives at FUSE level (both cgofuse + hanwen), not affecting WebDAV
- Lazy initialization: buffer/goroutine only allocated on first read
- Size threshold: only files > buffer size get buffered (skips Finder metadata)
- Configurable via async_buffer_size in FuseConfig (default 8MB, 0 to disable)
- 1MB fill chunks to reduce mutex overhead (1 lock per ~segment)

Results: FUSE read speed improved from ~24 MB/s to ~41 MB/s (within 4% of
rclone mount speed), with dramatically fewer stalls.
@javi11 javi11 merged commit 43d8013 into main Apr 12, 2026
2 checks passed
@javi11 javi11 deleted the feat/fuse-async-read-buffer branch April 12, 2026 21:05
javi11 added a commit that referenced this pull request Apr 14, 2026
…493)

Add AsyncReadBuffer at the FUSE layer that wraps ReadAtContext with a
background goroutine filling a ring buffer. FUSE reads pull from
pre-filled memory instead of blocking on NNTP segment downloads.

Before: ~24 MB/s with 66% stall time (reads blocked on segment downloads)
After: ~41 MB/s, matching rclone mount speed within 4%

Key design:
- Ring buffer (8MB default) with background fill goroutine
- Seek-aware: non-sequential reads reset buffer via generation counter
- Lazy initialization: no memory allocated until first read
- Size threshold: only files > buffer size get buffered (skips Finder)
- Lives at FUSE level only — WebDAV path unaffected
- Configurable via async_buffer_size in FuseConfig (0 to disable)
- Frontend UI added for the new setting

Also:
- Reduce default max_prefetch from 60 to 30 segments (async buffer
  covers the gap, halves segment memory per reader)
- Remove WarmUp() which raced with the async buffer
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.

1 participant