Skip to content

CFile: 64 KB userspace write buffer to coalesce metadata-save syscalls (#562) - #573

Merged
mrjimenez merged 1 commit into
amule-project:masterfrom
got3nks:fix/cfile-buffered-writes
May 11, 2026
Merged

CFile: 64 KB userspace write buffer to coalesce metadata-save syscalls (#562)#573
mrjimenez merged 1 commit into
amule-project:masterfrom
got3nks:fix/cfile-buffered-writes

Conversation

@got3nks

@got3nks got3nks commented May 10, 2026

Copy link
Copy Markdown
Contributor

Summary

CFile::doWrite() was a thin wrapper over ::write(m_fd, ...) — every CFileDataIO::WriteTag / WriteString / Write call round-tripped through the kernel. For CKnownFileList::Save() with hundreds of thousands of files (each carrying ~10 sub-tags), this meant millions of small write() syscalls and a 9-minute shutdown freeze on Linux/HDD setups (slrslr's report on #562, gdb backtrace pinned the main thread inside __GI___libc_writeCFile::doWriteCFileDataIO::WriteTagCKnownFile::WriteToFileCKnownFileList::Save; strace -c showed 100% CPU time in write syscalls at ~26k/s).

Fix

Add a 64 KB userspace write buffer to CFile. doWrite() accumulates into the buffer and flushes when full or when a single payload exceeds it. DrainWriteBuffer() is called from every path that needs the file's on-disk state to reflect preceding writes — Close, Flush, doSeek, doRead, GetPosition, GetLength, SetLength — so the buffer never holds bytes hostage past any observable boundary.

Read-only files bypass the buffer entirely (via a m_canBuffer flag set in Open() based on OpenMode). This preserves the contract that writing to a read-opened file fails immediately at the call site — see FileDataIOTest's CFile.Constructor ASSERT_RAISES(CIOFailureException, file.WriteUInt8(0)).

Verification

  • Existing unit tests pass (FileDataIOTest covers Read / Write / Seek / SetLength / Read-only / String / LargeFile across CFile and CMemFile). Both Mac (wxOSX) and Linux (wxGTK) full ctest passes — 9/9.
  • On a synthetic 70k-file shared library, CKnownFileList::Save()'s syscall count drops from O(millions) to O(thousands) (one syscall per filled 64 KB buffer instead of one per tag/string field).

Caveats

Multi-platform: identical code path on Linux, macOS, Windows (mingw). The buffer is a fixed-size char[] member — 64 KB per open CFile instance, paid only when files are actually open. Typical aMule has a handful of open CFile objects at a time (.met files, log targets), so the steady-state RSS impact is < 1 MB.

The fix doesn't change wire semantics or file format. Any program reading a .met file produced by the patched daemon sees identical bytes — only the kernel-call pattern producing them changes.

amule-project#562)

CFile::doWrite() was a thin wrapper over ::write(m_fd, ...), so every
CFileDataIO::WriteTag / WriteString / Write call round-tripped
through the kernel. For CKnownFileList::Save() with hundreds of
thousands of files (each carrying ~10 sub-tags), this meant millions
of small write() syscalls and a 9-minute shutdown freeze on Linux/HDD
setups (slrslr's report on amule-project#562; gdb backtrace pinned the main thread
inside __GI___libc_write → CFile::doWrite → CFileDataIO::WriteTag →
CKnownFile::WriteToFile → CKnownFileList::Save; strace -c showed
100%% CPU time in write syscalls at ~26k/s).

Add a 64 KB userspace write buffer to CFile. doWrite() accumulates
into the buffer and flushes when full or when a single payload
exceeds it. DrainWriteBuffer() is called from every path that needs
the file's on-disk state to reflect preceding writes — Close, Flush,
doSeek, doRead, GetPosition, GetLength, SetLength — so the buffer
never holds bytes hostage past any observable boundary. ~CFile()
calls Close(), so even an abandoned stack-allocated CFile drains.

Read-only files bypass the buffer entirely (via m_canBuffer flag set
in Open() based on OpenMode). This preserves the contract that
writing to a read-opened file fails immediately at the call site —
see FileDataIOTest's CFile.Constructor ASSERT_RAISES(...,
file.WriteUInt8(0)).

Multi-platform: identical code on Linux, macOS, Windows (mingw). The
buffer is a fixed-size char[] member — 64 KB per open CFile, paid
only when files are actually open. Typical aMule has a handful of
open CFile objects at a time (.met files, log targets), so steady-
state RSS impact is < 1 MB.

Doesn't change wire semantics or file format. Any program reading a
.met file produced by the patched daemon sees identical bytes — only
the kernel-call pattern producing them changes.

Existing unit tests pass on Mac (wxOSX) and Linux (wxGTK) — 9/9
ctest. FileDataIOTest covers the relevant Read / Write / Seek /
SetLength / read-only / String / LargeFile paths across CFile and
CMemFile.
@mifritscher2

Copy link
Copy Markdown

I see at least no regression or obviously broken met files after 2 restarts with this PR ;-) Speed was fine for me even before.

@mrjimenez
mrjimenez merged commit 85ace58 into amule-project:master May 11, 2026
12 checks passed
@got3nks
got3nks deleted the fix/cfile-buffered-writes branch May 11, 2026 08:01
got3nks added a commit to got3nks/amule that referenced this pull request Jul 24, 2026
…ect#573)

* feat(amulegui): wire the Kad "More" search button over EC

* chore(i18n): regenerate app catalogs for the moved Kad-search strings
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.

3 participants