Port eMule CUploadDiskIOThread + adaptive chunks and ASIO race/EPOLLET fixes - #451
Conversation
Offload file reads and packet creation from the main thread to a
dedicated disk I/O thread, ported from eMule's CUploadDiskIOThread.
Additionally fixes a latent RC4 stream-desync race in the ASIO socket
layer, fixes an EPOLLET spurious-wakeup bug in HandleRead that killed
read pipelines under high load, and introduces adaptive per-slot
packet chunk sizing that raises upload throughput 3-4x on fast links
without hurting slow peers.
--- Disk I/O thread ---
New CUploadDiskIOThread (src/UploadDiskIOThread.{h,cpp}) owns all
disk reads and ed2k packet construction for uploads. The main thread
no longer blocks on file I/O or zlib compression during upload slot
service. Key design choices vs eMule:
* wxThread + wxCondition replaces CWinThread + WaitForMultipleObjects
* Synchronous CFileArea::ReadAt() replaces ReadFile(OVERLAPPED) + IOCP
(no need for a pending-IO list; reads go straight to finished list)
* Sticky wake flags prevent lost signals from wxCondition's pulse
semantics (m_bNewBlocksPending, m_bSocketNeedsPending under
m_mutex)
* Upload list protected by wxMutex for cross-thread iteration safety
Converges several behaviours with eMule that aMule previously lacked:
* AddReqBlock: validates upload list membership, file availability,
block bounds, IsDownloading, and m_bIOError before queueing
* AddReqBlock: bSignalIOThread param for batched block requests
* ReadCompletionRoutine: HasQueues()/IsBusyQuickCheck() for the
compression starvation check (replaces aMule's older HasSent())
* m_bDisableCompression: persistent per-slot flag, once tripped the
slot uses uncompressed packets for the session
* m_bIOError: set by the disk thread when a read fails, consumed by
the upload queue Process() loop to drop the slot cleanly
* CreateStandardPackets / CreatePackedPackets moved from
CUpDownClient to static methods on CUploadDiskIOThread (matches
eMule's code organisation, lets the disk thread run without
holding client state)
* Disk I/O thread wait timeout aligned with eMule (500 ms)
Adopts two eMule 0.70b optimisations:
* Compression level lowered from 9 to 1 — for typical blocks the
size difference is small (~4-12%) but level 1 is 1.5-2.5x faster
* Small-block threshold: when a block's remainder is below
chunkSize + 2600 bytes, send it in one packet rather than
splitting into tiny fragments (eMule uses a fixed 13000 against
its 10240 chunk; this generalises to any chunk size)
--- ASIO RC4 stream desync (pre-existing bug) ---
In EMSocket.cpp and LibSocketAsio.cpp, m_sendBuffer and m_blocksWrite
were shared between the throttler thread and the ASIO thread pool
without synchronisation. eMule does not have this issue because it
uses synchronous Winsock Send() on a single thread; the race is
specific to aMule's Boost.ASIO async model and was latent until the
disk I/O thread added enough write pressure to expose it.
DispatchWrite() read m_sendBuffer from the strand, but the throttler
thread could have already replaced it — causing the same encrypted
data to be sent twice and desynchronising the RC4 cipher against the
peer. Additionally, CEMSocket::Send() advanced 'sent' only after
BlocksWrite() which could return a stale value and cause the same
bytes to be re-sent.
Fix: make m_sendBuffer and m_blocksWrite std::atomic, capture the
buffer pointer at dispatch time and pass it explicitly through to
HandleSend, use compare_exchange_strong in HandleSend to clear
m_sendBuffer only if it still points to the buffer we just sent, and
advance 'sent' before checking BlocksWrite() in EMSocket::Send().
--- EPOLLET spurious wakeup ---
CAsioSocketImpl::HandleRead() had two latent issues under the
4-thread io_service that became reproducible at high upload rates:
1. available()/read_some() race. available() sized the buffer,
then read_some(avail) consumed it. If more data arrived between
those two calls, it stayed in the kernel buffer: boost.asio uses
EPOLLET (edge-triggered) and the kernel does not re-fire the
read-ready event unless the buffer empties and then refills.
The socket would stall indefinitely with data pending.
Fix: loop available()+read_some() until drained.
2. available()==0 was unconditionally treated as "peer closed".
But the same condition triggers on spurious wakeups — boost.asio
can schedule a read handler for a queued edge event that was
already consumed by a concurrent drain on another ASIO thread.
The old code called SetError()/PostLostEvent(), permanently
killing the socket's read pipeline even though the connection
was alive.
Fix: probe with ::recv(MSG_PEEK|MSG_DONTWAIT) to distinguish
real EOF (returns 0) from a spurious wakeup (returns -1 with
EAGAIN/EWOULDBLOCK). On spurious, re-arm the async_read_some
and return without error.
Reproducer: with 32 KiB upload chunks and ~50+ MB/s upload throughput
on the dev VM, the bug triggers within 30-60 s; with eMule's default
10 KiB chunks it is much rarer but still latent.
Note: m_socket->non_blocking() in the constructor is a getter, not a
setter — the socket stays in blocking mode. The raw native_handle +
MSG_DONTWAIT is therefore necessary for a guaranteed non-blocking
probe.
--- Adaptive chunk size (10-128 KiB) ---
Replaces eMule's fixed 10 KiB packet size with a per-slot formula:
chunkSize = clamp(uploadDatarate / 8, 10240, 131072)
At session start (datarate=0) the floor keeps behaviour at eMule's
default 10 KiB. As the slot ramps up, chunks grow with throughput —
/8 targets ~125 ms of data per packet, enough to minimise per-packet
overhead (ed2k framing, encryption, syscalls) without making per-
packet latency pathological on slow peers. Capped at 128 KiB to
stay well within typical TCP send-buffer sizes.
uploadDatarate is plumbed from ReadCompletionRoutine via
CUpDownClient::GetUploadDatarate() into both CreateStandardPackets
(uncompressed) and CreatePackedPackets (compressed).
Measured on the dev VM (6.3 GB upload against a Python test client):
eMule 10 KiB fixed: ~15-18 MB/s ceiling
Adaptive: 67.3 MB/s avg, 73.5 MB/s peak
Chunk count dropped from ~180k to ~40k for the same payload, meaning
the /8 formula saturated at the 128 KiB ceiling for most of the
transfer.
|
I just conducted a short smoke test and can confirm that both down- and uploading are working well (Debian Trixie). Download rate was about 3 MB/s, Upload about 0,7 MB/s (both limited by the network), transfer of 2 files at the same time. One original and one patched client. |
|
Thanks for testing @mifritscher2 ! Great to hear both directions work on Debian Trixie. To see the throughput improvements from the adaptive chunking and disk I/O thread, you can use the benchmark script against your own amuled instance on localhost. It speaks the full ed2k obfuscated protocol and simulates a downloading client: Benchmark script: https://gist.github.com/got3nks/3e7661add07ba862a5ead0aa2e85ff28 Quick test (requires a shared file and pycryptodome for best performance): |
|
Works fine as well. If I limit the upload rate to 2000 KB/s, I get 1,96 MB/s. |
The receiver requests blocks of EMBLOCKSIZE (180 KiB). Chunks larger than a block have no effect since each block is sent as a single packet when chunkSize >= togo. The previous 128 KiB ceiling caused each 180 KiB block to be split into two packets unnecessarily. Ceiling changed from the magic constant 131072 to EMBLOCKSIZE so the chunk size naturally tracks the protocol's block size. At fast upload rates (>= 1.4 MB/s) each block is now sent as one packet instead of two, halving per-block packet overhead.
OnExit() deleted uploadqueue before calling uploadDiskIOThread->EndThread(). If the disk I/O thread was mid-iteration over uploadqueue->GetUploadingList() when uploadqueue was freed, it would access dangling memory and crash (wxFatalSignalHandler → abort). Fix: stop uploadDiskIOThread first, before freeing uploadqueue.
The EPOLLET spurious-wakeup workaround added in the CUploadDiskIOThread port uses <sys/socket.h>, <unistd.h>, and ::recv() with MSG_PEEK | MSG_DONTWAIT. That all works on Linux but breaks the Windows MinGW compile: <sys/socket.h> doesn't exist, and MSG_DONTWAIT isn't a defined flag in WinSock2. boost::asio on Windows uses IOCP rather than EPOLLET, so the underlying reason for the peek-recv dance — a completion handler firing with zero bytes after a concurrent drain on another ASIO thread consumed the data — does not apply. An async_read_some completion with zero bytes on IOCP cleanly means EOF. - Guard the POSIX headers with #ifdef _WIN32, including <winsock2.h> on Windows instead. - On Windows, treat "avail == 0 in a read completion" as EOF directly, skipping the peek recv. Linux/Mac keep the existing behaviour.
Two tables at the top of the Performance section in 3.0.0:
1. Cross-platform end-to-end (2.3.3 vs 3.0.0) — sustained download
throughput on macOS / Linux ARM / Windows ARM, peer-to-peer over
LAN, 30 GB file:
macOS: 0.35 -> 135 MB/s (381x)
Linux: 0.34 -> 117 MB/s (345x)
Windows: 0.36 -> 39 MB/s (107x)
Plus a per-platform seeder/leecher contribution split that shows
the seeder-side fix (amule-project#451) dominates and the leecher-side stack
(amule-project#454/amule-project#484/amule-project#491) adds another 4.5-5.8x on top.
2. vs eMule 0.70b on Windows — same UTM hardware, both directions:
Upload (Windows seeds -> Mac leecher): 22 vs 106 MB/s (~4.8x)
Download (Linux seeds -> Windows leech): 20 vs 39 MB/s (~1.9x)
Methodology footnote on the table since eMule has no remote-
control protocol and is measured leecher-side via aMule's EC
channel on the receiving box.
Highlights line 16 also updated to mention both directions of the
eMule comparison instead of just download.
The existing per-PR benches in #### Upload / #### Download stay
unchanged — those are PR-specific numbers documenting how each
single change contributed; the new tables document the end-to-end
2.3.3-to-3.0.0 user experience.
Numbers measured with the bench-matrix.sh dispatcher
(scripts/bench-matrix.sh) — 12-run aMule-vs-aMule matrix plus 2
manual aMule-vs-eMule runs. Raw logs in
bench-results/bench-matrix-20260505T141651Z/.
Two tables at the top of the Performance section in 3.0.0:
1. Cross-platform end-to-end (2.3.3 vs 3.0.0) — sustained download
throughput on macOS / Linux ARM / Windows ARM, peer-to-peer over
LAN, 30 GB file:
macOS: 0.35 -> 135 MB/s (381x)
Linux: 0.34 -> 117 MB/s (345x)
Windows: 0.36 -> 39 MB/s (107x)
Plus a per-platform seeder/leecher contribution split that shows
the seeder-side fix (amule-project#451) dominates and the leecher-side stack
(amule-project#454/amule-project#484/amule-project#491) adds another 4.5-5.8x on top.
2. vs eMule 0.70b on Windows — same UTM hardware, both directions:
Upload (Windows seeds -> Mac leecher): 22 vs 106 MB/s (~4.8x)
Download (Linux seeds -> Windows leech): 20 vs 39 MB/s (~1.9x)
Methodology footnote on the table since eMule has no remote-
control protocol and is measured leecher-side via aMule's EC
channel on the receiving box.
Highlights line 16 also updated to mention both directions of the
eMule comparison instead of just download.
The existing per-PR benches in #### Upload / #### Download stay
unchanged — those are PR-specific numbers documenting how each
single change contributed; the new tables document the end-to-end
2.3.3-to-3.0.0 user experience.
Numbers measured with the bench-matrix.sh dispatcher
(scripts/bench-matrix.sh) — 12-run aMule-vs-aMule matrix plus 2
manual aMule-vs-eMule runs. Raw logs in
bench-results/bench-matrix-20260505T141651Z/.
Two tables at the top of the Performance section in 3.0.0:
1. Cross-platform end-to-end (2.3.3 vs 3.0.0) — sustained download
throughput on macOS / Linux ARM / Windows ARM, peer-to-peer over
LAN, 30 GB file:
macOS: 0.35 -> 135 MB/s (381x)
Linux: 0.34 -> 117 MB/s (345x)
Windows: 0.36 -> 39 MB/s (107x)
Plus a per-platform seeder/leecher contribution split that shows
the seeder-side fix (amule-project#451) dominates and the leecher-side stack
(amule-project#454/amule-project#484/amule-project#491) adds another 4.5-5.8x on top.
2. vs eMule 0.70b on Windows — same UTM hardware, both directions:
Upload (Windows seeds -> Mac leecher): 22 vs 106 MB/s (~4.8x)
Download (Linux seeds -> Windows leech): 20 vs 39 MB/s (~1.9x)
Methodology footnote on the table since eMule has no remote-
control protocol and is measured leecher-side via aMule's EC
channel on the receiving box.
Highlights line 16 also updated to mention both directions of the
eMule comparison instead of just download.
The existing per-PR benches in #### Upload / #### Download stay
unchanged — those are PR-specific numbers documenting how each
single change contributed; the new tables document the end-to-end
2.3.3-to-3.0.0 user experience.
Numbers measured with the bench-matrix.sh dispatcher
(scripts/bench-matrix.sh) — 12-run aMule-vs-aMule matrix plus 2
manual aMule-vs-eMule runs. Raw logs in
bench-results/bench-matrix-20260505T141651Z/.
Two tables at the top of the Performance section in 3.0.0:
1. Cross-platform end-to-end (2.3.3 vs 3.0.0) — sustained download
throughput on macOS / Linux ARM / Windows ARM, peer-to-peer over
LAN, 30 GB file:
macOS: 0.35 -> 135 MB/s (381x)
Linux: 0.34 -> 117 MB/s (345x)
Windows: 0.36 -> 39 MB/s (107x)
Plus a per-platform seeder/leecher contribution split that shows
the seeder-side fix (amule-project#451) dominates and the leecher-side stack
(amule-project#454/amule-project#484/amule-project#491) adds another 4.5-5.8x on top.
2. vs eMule 0.70b on Windows — same UTM hardware, both directions:
Upload (Windows seeds -> Mac leecher): 22 vs 106 MB/s (~4.8x)
Download (Linux seeds -> Windows leech): 20 vs 39 MB/s (~1.9x)
Methodology footnote on the table since eMule has no remote-
control protocol and is measured leecher-side via aMule's EC
channel on the receiving box.
Highlights line 16 also updated to mention both directions of the
eMule comparison instead of just download.
The existing per-PR benches in #### Upload / #### Download stay
unchanged — those are PR-specific numbers documenting how each
single change contributed; the new tables document the end-to-end
2.3.3-to-3.0.0 user experience.
Numbers measured with the bench-matrix.sh dispatcher
(scripts/bench-matrix.sh) — 12-run aMule-vs-aMule matrix plus 2
manual aMule-vs-eMule runs. Raw logs in
bench-results/bench-matrix-20260505T141651Z/.
Two tables at the top of the Performance section in 3.0.0:
1. Cross-platform end-to-end (2.3.3 vs 3.0.0) — sustained download
throughput on macOS / Linux ARM / Windows ARM, peer-to-peer over
LAN, 30 GB file:
macOS: 0.35 -> 135 MB/s (381x)
Linux: 0.34 -> 117 MB/s (345x)
Windows: 0.36 -> 39 MB/s (107x)
Plus a per-platform seeder/leecher contribution split that shows
the seeder-side fix (#451) dominates and the leecher-side stack
(#454/#484/#491) adds another 4.5-5.8x on top.
2. vs eMule 0.70b on Windows — same UTM hardware, both directions:
Upload (Windows seeds -> Mac leecher): 22 vs 106 MB/s (~4.8x)
Download (Linux seeds -> Windows leech): 20 vs 39 MB/s (~1.9x)
Methodology footnote on the table since eMule has no remote-
control protocol and is measured leecher-side via aMule's EC
channel on the receiving box.
Highlights line 16 also updated to mention both directions of the
eMule comparison instead of just download.
The existing per-PR benches in #### Upload / #### Download stay
unchanged — those are PR-specific numbers documenting how each
single change contributed; the new tables document the end-to-end
2.3.3-to-3.0.0 user experience.
Numbers measured with the bench-matrix.sh dispatcher
(scripts/bench-matrix.sh) — 12-run aMule-vs-aMule matrix plus 2
manual aMule-vs-eMule runs. Raw logs in
bench-results/bench-matrix-20260505T141651Z/.
…mule-project#445) (amule-project#471) The per-poll log batching added in amule-project#451 scrolls the "aMule Log" text ctrl (EndLogBatch) while it is still frozen, then thaws. On Windows, ShowPosition() on a frozen wxTE_RICH2 control doesn't lay out the visible region, so after Thaw the view comes back blank -- only the last line pinned at the top -- until a manual scroll forces a repaint. Since every stats poll carrying new daemon log lines runs through this path, the log window blanked on each new line. Thaw the control before scrolling so ShowPosition operates on a live control. Keeps the batching win (Freeze still spans the appends); macOS/GTK, which tolerate scroll-while-frozen, are unaffected either way.
…ject#445) + list rows blanking on scroll/keyboard (amule-project#478) (amule-project#477) * fix(remote-gui): stop the log view blanking on Windows by dropping Freeze/Thaw (amule-project#445) amule-project#471 tried to fix the "log view goes blank on every new line" Windows regression by thawing before scrolling, but it made no difference: the AppendText still ran while the control was frozen. Appending to a frozen wxTE_RICH2 (RichEdit) on Windows leaves its line/scroll metrics stale, so on Thaw the view renders blank with the newest line pinned to the top until a manual scroll forces a recompute. The Freeze()/Thaw() that amule-project#451 wrapped the per-poll appends in is the actual culprit, not the scroll order. Drop the Freeze()/Thaw() and append on a live control (as the pre-amule-project#451 code did). Keep the two real wins from amule-project#451: the daemon's 5000-lines-per-poll cap and the conditional SetDefaultStyle, plus one coalesced ShowPosition per poll instead of per line (m_logBatching still suppresses the per-line scroll). The per-line SetDefaultStyle was the dominant first-sync cost, so responsiveness is retained without the frozen-append rendering corruption. macOS/GTK recompute metrics regardless and were unaffected either way. * fix(remote-gui): repaint list rows on scroll & keyboard nav on Windows (amule-project#478) amule-project#348 replaced the vendored wxListMainWindow::OnScroll's synchronous HandleOnScroll(event) with event.Skip() -- HandleOnScroll became a private member in wx 3.3.3, so the macOS/Linux CI (on wx 3.3.x) stopped compiling. On wxMSW the resulting deferred-scroll path (wxScrollHelperBase::HandleOnScroll -> ScrollWindow) blits the retained rows and only invalidates the newly exposed strip, which is then left unpainted: rows go blank on mouse-wheel, scrollbar and pagination scrolling until a redraw is forced. Verified in the wx 3.2.10 and 3.3.x sources that the scroll+repaint mechanism is identical, so this is a wxMSW platform behavior, not a wx-version one. Rather than gate on platform or wx version (both fragile -- the former needs the now-private HandleOnScroll, the latter breaks once Windows ships wx 3.3.3), reimplement the synchronous scroll using only the public wxScrollHelper API (GetViewStart / GetScrollLines / GetScrollPageSize / Scroll), mirroring HandleOnScroll()/CalcScrollInc(): translate the scroll event into a target position in scroll units and scroll to it. Factor the "scroll + repaint" sequence into a shared ScrollListTo(x, y) helper (Update() to flush pending paints so the blit is clean, Scroll(), then ResetVisibleLinesRange() so the exposed rows repaint) and route both OnScroll (scrollbar/wheel) and MoveToItem (keyboard nav) through it. This also fixes the keyboard HOME/END/PgUp/PgDn blanking, which was the same bug from a different path: MoveToItem only reset the visible-line range after Scroll() under __WXMAC__, so on Windows the range stayed stale and keyboard scrolls came up blank. The reset now runs on every platform via the shared helper, and the old __WXMAC__-only workaround is gone. Not skipping the event means the base scroll helper won't also scroll, so no double-scroll. Works on every supported wx (>= 3.2.0) and every platform with no version or platform guard; compiles against wx 3.3.3. Refs amule-project#478
The amuleGUI log panes used a wxTE_RICH2 (RichEdit) control, which holds the whole document and reflows O(n) on scroll. A remote-GUI first sync can dump tens of thousands of daemon-log lines at once, so scrolling crawled and the view mispainted -- only the tail visible until a manual one-line scroll (issues amule-project#445, amule-project#547). Replace the three log/info panes (aMule Log, aMuleGUI Log, server info) with a new CMuleLogCtrl backed by wxStyledTextCtrl. Scintilla renders only the visible lines, so scrolling and full-history retention stay O(visible) at any size, and unlike a virtual list it keeps character-level selection and find. Critical lines are bold via per-line Scintilla styles; the view tail-scrolls only when already at the bottom, so scrolling up to read history sticks. This removes the whole RichEdit workaround stack -- the per-poll append batching, the deferred hidden-page scroll, and the Freeze/Thaw lineage from amule-project#451/amule-project#471/amule-project#477 -- none of which Scintilla needs. wxStyledTextCtrl (the stc component) ships with the wxWidgets packages every platform already builds against, so it adds no new dependency; it is requested only for the GUI library that owns the log views.
The amuleGUI log panes used a wxTE_RICH2 (RichEdit) control, which holds the whole document and reflows O(n) on scroll. A remote-GUI first sync can dump tens of thousands of daemon-log lines at once, so scrolling crawled and the view mispainted -- only the tail visible until a manual one-line scroll (issues amule-project#445, amule-project#547). Replace the three log/info panes (aMule Log, aMuleGUI Log, server info) with a new CMuleLogCtrl backed by wxStyledTextCtrl. Scintilla renders only the visible lines, so scrolling and full-history retention stay O(visible) at any size, and unlike a virtual list it keeps character-level selection and find. Critical lines are bold via per-line Scintilla styles; the view tail-scrolls only when already at the bottom, so scrolling up to read history sticks. This removes the whole RichEdit workaround stack -- the per-poll append batching, the deferred hidden-page scroll, and the Freeze/Thaw lineage from amule-project#451/amule-project#471/amule-project#477 -- none of which Scintilla needs. wxStyledTextCtrl (the stc component) ships with the wxWidgets packages every platform already builds against, so it adds no new dependency; it is requested only for the GUI library that owns the log views.
The amuleGUI log panes used a wxTE_RICH2 (RichEdit) control, which holds the whole document and reflows O(n) on scroll. A remote-GUI first sync can dump tens of thousands of daemon-log lines at once, so scrolling crawled and the view mispainted -- only the tail visible until a manual one-line scroll (issues amule-project#445, amule-project#547). Replace the three log/info panes (aMule Log, aMuleGUI Log, server info) with a new CMuleLogCtrl backed by wxStyledTextCtrl. Scintilla renders only the visible lines, so scrolling and full-history retention stay O(visible) at any size, and unlike a virtual list it keeps character-level selection and find. Critical lines are bold via per-line Scintilla styles; the view tail-scrolls only when already at the bottom, so scrolling up to read history sticks. This removes the whole RichEdit workaround stack -- the per-poll append batching, the deferred hidden-page scroll, and the Freeze/Thaw lineage from amule-project#451/amule-project#471/amule-project#477 -- none of which Scintilla needs. wxStyledTextCtrl (the stc component) ships with the wxWidgets packages every platform already builds against, so it adds no new dependency; it is requested only for the GUI library that owns the log views.
The amuleGUI log panes used a wxTE_RICH2 (RichEdit) control, which holds the whole document and reflows O(n) on scroll. A remote-GUI first sync can dump tens of thousands of daemon-log lines at once, so scrolling crawled and the view mispainted -- only the tail visible until a manual one-line scroll (issues amule-project#445, amule-project#547). Replace the three log/info panes (aMule Log, aMuleGUI Log, server info) with a new CMuleLogCtrl backed by wxStyledTextCtrl. Scintilla renders only the visible lines, so scrolling and full-history retention stay O(visible) at any size, and unlike a virtual list it keeps character-level selection and find. Lines word-wrap as the old pane did; critical lines are bold via per-line Scintilla styles. The view tail-scrolls after new content only when it was already at the bottom, so scrolling up to read history sticks. When the backlog arrives while the Networks page is hidden (the default tab on launch is Transfer) the log control has no laid-out geometry and cannot be scrolled reliably, so the tail-scroll is deferred and performed when the page is next shown. This removes the whole RichEdit workaround stack -- the per-poll append batching and the Freeze/Thaw lineage from amule-project#451/amule-project#471/amule-project#477 -- none of which Scintilla needs. wxStyledTextCtrl (the stc component) ships with the wxWidgets packages every platform already builds against, so it adds no new dependency; it is requested only for the GUI library that owns the log views.
The amuleGUI log panes used a wxTE_RICH2 (RichEdit) control, which holds the whole document and reflows O(n) on scroll. A remote-GUI first sync can dump tens of thousands of daemon-log lines at once, so scrolling crawled and the view mispainted -- only the tail visible until a manual one-line scroll (issues amule-project#445, amule-project#547). Replace the three log/info panes (aMule Log, aMuleGUI Log, server info) with a new CMuleLogCtrl backed by wxStyledTextCtrl. Scintilla renders only the visible lines, so scrolling and full-history retention stay O(visible) at any size, and unlike a virtual list it keeps character-level selection and find. Lines word-wrap as the old pane did; critical lines are bold via per-line Scintilla styles. The view tail-scrolls after new content only when it was already at the bottom, so scrolling up to read history sticks. When lines arrive while a pane is hidden (its notebook page or sub-tab is not selected -- e.g. the first-sync backlog before the Networks tab is opened) the control has no laid-out geometry and cannot be scrolled reliably, so the tail-scroll is deferred and applied on the first idle once the pane is on screen. This lives in CMuleLogCtrl, so all three panes share it. This removes the whole RichEdit workaround stack -- the per-poll append batching and the Freeze/Thaw lineage from amule-project#451/amule-project#471/amule-project#477 -- none of which Scintilla needs. wxStyledTextCtrl (the stc component) ships with the wxWidgets packages every platform already builds against, so it adds no new dependency; it is requested only for the GUI library that owns the log views.
…le-project#548) The amuleGUI log panes used a wxTE_RICH2 (RichEdit) control, which holds the whole document and reflows O(n) on scroll, so a remote-GUI first-sync backlog of tens of thousands of daemon-log lines made scrolling crawl and mispaint (only the tail visible until a manual scroll). Replace the three log/info panes (aMule Log, aMuleGUI Log, server info) with a new CMuleLogCtrl backed by wxStyledTextCtrl (Scintilla), which renders only the visible lines: full history is kept and scrolling stays O(visible) at any size, with character-level selection/find preserved. Lines word-wrap; critical lines are bold via per-line styles. Tail-scroll only fires when already at the bottom; a scroll requested while the pane is hidden is deferred to the first idle once it is on screen (in the base control, so all three panes share it). Removes the RichEdit workaround stack (per-poll batching, the Freeze/Thaw lineage of amule-project#451/amule-project#471/amule-project#477). The stc component ships with every platform's wxWidgets and is requested only for the GUI library; the Windows portable bundles its DLL via the existing GET_RUNTIME_DEPENDENCIES install step. Reported in amule-project#547.
Summary
Offloads disk reads and ed2k packet construction from the main thread to a dedicated disk I/O thread (ported from eMule's
CUploadDiskIOThread), fixes a latent RC4 stream-desync race in the ASIO socket layer, fixes an EPOLLET spurious-wakeup bug inHandleReadthat killed read pipelines under high load, and introduces adaptive per-slot packet chunk sizing that raises upload throughput 3-4x on fast links without hurting slow peers.What's in this PR
1. Disk I/O thread
New
CUploadDiskIOThread(src/UploadDiskIOThread.{h,cpp}) owns all disk reads and ed2k packet construction for uploads. The main thread no longer blocks on file I/O or zlib compression during upload slot service.Design choices vs eMule:
wxThread+wxConditionreplacesCWinThread+WaitForMultipleObjectsCFileArea::ReadAt()replacesReadFile(OVERLAPPED)+ IOCP (no need for a pending-IO list; reads go straight to the finished list)wxCondition's pulse semantics (m_bNewBlocksPending,m_bSocketNeedsPendingunderm_mutex)wxMutexfor cross-thread iteration safetyConverges several behaviours with eMule that aMule previously lacked:
AddReqBlock: validates upload list membership, file availability, block bounds,IsDownloading, andm_bIOErrorbefore queueingAddReqBlock:bSignalIOThreadparam for batched block requestsReadCompletionRoutine:HasQueues()/IsBusyQuickCheck()for the compression starvation check (replaces aMule's olderHasSent())m_bDisableCompression: persistent per-slot flag; once tripped the slot uses uncompressed packets for the sessionm_bIOError: set by the disk thread when a read fails, consumed by the upload queueProcess()loop to drop the slot cleanlyCreateStandardPackets/CreatePackedPacketsmoved fromCUpDownClientto static methods onCUploadDiskIOThread(matches eMule's code organisation; lets the disk thread run without holding client state)Adopts two eMule 0.70b optimisations:
chunkSize + 2600bytes, send it in one packet rather than splitting into tiny fragments (eMule uses a fixed 13000 against its 10240 chunk; this generalises to any chunk size)2. ASIO RC4 stream desync fix (pre-existing bug)
In
EMSocket.cppandLibSocketAsio.cpp,m_sendBufferandm_blocksWritewere shared between the throttler thread and the ASIO thread pool without synchronisation. eMule does not have this issue because it uses synchronous WinsockSend()on a single thread; the race is specific to aMule's Boost.ASIO async model and was latent until the disk I/O thread added enough write pressure to expose it.DispatchWrite()readm_sendBufferfrom the strand, but the throttler thread could have already replaced it — causing the same encrypted data to be sent twice and desynchronising the RC4 cipher against the peer. Additionally,CEMSocket::Send()advancedsentonly afterBlocksWrite()which could return a stale value and cause the same bytes to be re-sent.Fix:
m_sendBufferandm_blocksWritestd::atomicHandleSendcompare_exchange_stronginHandleSendto clearm_sendBufferonly if it still points to the buffer we just sentsentbefore checkingBlocksWrite()inEMSocket::Send()3. EPOLLET spurious-wakeup fix
CAsioSocketImpl::HandleRead()had two latent issues under the 4-threadio_servicethat became reproducible at high upload rates:a.
available()/read_some()race.available()sized the buffer, thenread_some(avail)consumed it. If more data arrived between those two calls, it stayed in the kernel buffer: boost.asio usesEPOLLET(edge-triggered) and the kernel does not re-fire the read-ready event unless the buffer empties and then refills. The socket would stall indefinitely with data pending. Fix: loopavailable()+read_some()until drained.b.
available()==0treated as "peer closed". The same condition triggers on spurious wakeups — boost.asio can schedule a read handler for a queued edge event that was already consumed by a concurrent drain on another ASIO thread. The old code calledSetError()/PostLostEvent(), permanently killing the socket's read pipeline even though the connection was alive. Fix: probe with::recv(MSG_PEEK | MSG_DONTWAIT)to distinguish real EOF (returns 0) from a spurious wakeup (returns -1 withEAGAIN/EWOULDBLOCK). On spurious, re-arm theasync_read_someand return without error.Note:
m_socket->non_blocking()in the constructor is a getter, not a setter — the socket stays in blocking mode. The rawnative_handle+MSG_DONTWAITis therefore necessary for a guaranteed non-blocking probe.Reproducer: with 32 KiB upload chunks and ~50+ MB/s upload throughput on a VM, the bug triggers within 30-60 s; with eMule's default 10 KiB chunks it is much rarer but still latent.
4. Adaptive chunk size (10 KiB – EMBLOCKSIZE)
Replaces eMule's fixed 10 KiB packet size with a per-slot formula:
At session start (
datarate=0) the floor keeps behaviour at eMule's default 10 KiB. As the slot ramps up, chunks grow with throughput —/8targets ~125 ms of data per packet, enough to minimise per-packet overhead (ed2k framing, encryption, syscalls) without making per-packet latency pathological on slow peers. Capped at EMBLOCKSIZE (180 KiB) — one packet per block. There is no benefit in going higher since the receiver requests blocks of this size.uploadDatarateis plumbed fromReadCompletionRoutineviaCUpDownClient::GetUploadDatarate()into bothCreateStandardPackets(uncompressed) andCreatePackedPackets(compressed).Benchmarks
Tested using a standalone Python benchmark client that speaks the ed2k obfuscated protocol and requests upload slots like a real aMule peer:
Benchmark script: https://gist.github.com/got3nks/3e7661add07ba862a5ead0aa2e85ff28
On a 1 Gbps Docker host (single client, 60 s, MaxUpload=65534)
Stock aMule master (same host, same peer, same benchmark, no changes from this PR):
With this PR (uint16 → uint32 widening from #436 not applied):
~130x throughput on the same host, same peer, same benchmark. With #436 applied, peak throughput scales further (gigabit-class links are limited by the uint16 configuration cap before it is widened).
Backward compatibility
Files changed
src/UploadDiskIOThread.hsrc/UploadDiskIOThread.cppsrc/UploadClient.cppAddReqBlockvalidation; moved packet helpers outsrc/updownclient.hm_bIOError,m_bDisableCompressionflagssrc/UploadQueue.cppIsDownloading; consumem_bIOErrorflagsrc/UploadQueue.hIsDownloadingaccessorsrc/UploadBandwidthThrottler.*src/EMSocket.cpp/.hHasQueues/IsBusyQuickChecksrc/LibSocketAsio.cppsrc/amule.cpp/.hsrc/Makefile.amUploadDiskIOThread.cppcmake/source-vars.cmakeUploadDiskIOThread.cpp(cmake)