download: enforce MaxDownload as a literal byte/sec global cap - #491
Merged
mrjimenez merged 1 commit intoApr 27, 2026
Merged
Conversation
The download throttle has never actually been a bandwidth cap. The
old code in DownloadQueue::Process computed `downspeed` as a
50-200% adaptive ratio against the observed aggregate datarate:
downspeed = MaxDownload * 102400 / (m_datarate + 1), clamped to [50, 200]
then in CUpDownClient::SetDownloadLimit converted that percentage
back to a per-tick byte budget by multiplying it against the
client's smoothed speed. That's a closed-loop controller that
nudges each peer's transfer rate by ~5%/tick relative to its own
current speed -- it does *not* enforce MaxDownload as a literal
ceiling. Setting MaxDownload=20000 (20 MB/s) wouldn't reliably
plateau at 20 MB/s; it just shaped traffic toward whatever the
aggregate datarate happened to converge on.
Replaced with a single global token bucket --
CDownloadBandwidthThrottler -- that every CEMSocket consults via
Reserve() before each Read(). The bucket is refilled once per
CORE_TIMER_PERIOD by DownloadQueue::Process from
MaxDownload * 1024 * tick_period / 1000. A small carry-over
(capped at 2x budget) keeps reads flowing across tick boundaries
so TCP flow control doesn't see receive-side stalls and back off.
Demand-aware redistribution falls out naturally: there's no
per-peer slice. A fast peer can claim unused capacity from slow
peers within the same tick; the global cap is the only constraint.
Knock-on cleanups:
- CEMSocket::SetDownloadLimit / DisableDownloadLimit replaced by
CEMSocket::WakeIfPaused -- the sole per-socket bit left is
pendingOnReceive (set when the bucket emptied mid-read, cleared
on the next successful read or wake).
- CUpDownClient::SetDownloadLimit(uint32) renamed to
TickDownloadAndMeasure() -- the parameter was the legacy ratio,
now nothing per-peer is being set.
- CPartFile::Process drops its reducedownload parameter; its only
job per-tick is to wake paused sockets and sum kBpsClient for
the kBpsDown display.
- DownloadClient.cpp's transition-out-of-DS_DOWNLOADING no longer
touches a per-socket cap; the global throttler is unaffected
by individual peer state changes.
Verified on macOS:
- MaxDownload=0 (UNLIMITED): 140.79 MB/s peak (was 140.27, no
regression on the unlimited path).
- MaxDownload=20000 (20 MB/s cap): 19.52 MB/s steady-state
(matches the per-peer strict-cap approach within 0.05 MB/s,
which itself was a meaningful improvement over the broken
ratio controller).
This was referenced Apr 29, 2026
13 tasks
got3nks
added a commit
to got3nks/amule
that referenced
this pull request
May 5, 2026
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/.
got3nks
added a commit
to got3nks/amule
that referenced
this pull request
May 5, 2026
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/.
got3nks
added a commit
to got3nks/amule
that referenced
this pull request
May 5, 2026
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/.
got3nks
added a commit
to got3nks/amule
that referenced
this pull request
May 5, 2026
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/.
mrjimenez
pushed a commit
that referenced
this pull request
May 6, 2026
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/.
This was referenced May 8, 2026
Open
mrjimenez
pushed a commit
to mrjimenez/amule
that referenced
this pull request
Jul 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
MaxDownloadhas never actually been a bandwidth cap. The old code inDownloadQueue::Processcomputed adownspeedpercentage as a 50-200% adaptive ratio against the observed aggregate datarate:then in
CUpDownClient::SetDownloadLimitconverted that percentage back to a per-tick byte budget by multiplying it against the client's smoothed speed. That's a closed-loop controller that nudges each peer's transfer rate by ~5%/tick relative to its own current speed — it does not enforceMaxDownloadas a literal ceiling. SettingMaxDownload=20000(20 MB/s) wouldn't reliably plateau at 20 MB/s; it just shaped traffic toward whatever the aggregate datarate happened to converge on, with the slow-ramp / plateau-below-target behaviour anyone who's set a non-zeroMaxDownloadhas noticed.Replaces the per-peer ratio controller with a single global token bucket —
CDownloadBandwidthThrottler— that everyCEMSocketconsults viaReserve()before eachRead(). The bucket is refilled once perCORE_TIMER_PERIODbyDownloadQueue::ProcessfromMaxDownload * 1024 * tick_period_ms / 1000. A small carry-over (capped at 2× budget) keeps reads flowing across tick boundaries so TCP flow control doesn't see receive-side stalls and back off.Demand-aware redistribution falls out naturally: there's no per-peer slice. A fast peer can claim unused capacity from slow peers within the same tick; the global cap is the only constraint.
What changed
src/DownloadBandwidthThrottler.{h,cpp}(new)Reserve(bytes),Refund(bytes),RefillBudget(KBps, tickMs).m_unlimitedflag short-circuits whenMaxDownload=0.src/EMSocket.{h,cpp}OnReceive()consultsCDownloadBandwidthThrottler::Reservebefore eachRead(); refunds the unused portion.SetDownloadLimit/DisableDownloadLimitremoved; replaced byWakeIfPaused()that retriesOnReceive(0)if the socket suspended last tick because the bucket was empty.downloadLimit/downloadLimitEnablemembers gone.src/DownloadQueue.cppCDownloadBandwidthThrottler::Get().RefillBudget(...)call per tick replaces the entiredownspeedratio block.src/PartFile.{h,cpp}Process(uint8 m_icounter)—reducedownloadparameter dropped; the loop only wakes paused sockets and sumskBpsClientfor the per-file display readout.src/BaseClient.cpp+src/updownclient.hCUpDownClient::SetDownloadLimit(uint32)renamed toTickDownloadAndMeasure()— the parameter was the legacy ratio, now nothing per-peer is being set.src/DownloadClient.cppDS_DOWNLOADINGm_socket->DisableDownloadLimit()call is gone — the cap is global, no per-socket state to clear on state changes.cmake/source-vars.cmakeDownloadBandwidthThrottler.cpptoCORE_SOURCES.Net diff:
+243 / -92lines across 11 files, single commit.Why a global bucket and not a per-peer share
Two reasons, both empirically validated below:
MaxDownloadis "do not exceed N KB/s total"; users don't want it to mean "cap each peer at N/k KB/s and waste the slack". A global bucket gives slow peers exactly what they can fill and lets fast peers absorb the rest.The original ratio controller dodged both of these by not actually capping anything — but that's not what the pref is documented to mean.
Bench
Test rig: an aMule seeder on a separate LAN host. Leech on each of macOS (Apple Silicon, native), Ubuntu ARM (UTM), Windows 11 ARM (UTM). Same revision on seeder and every leech. 5 GB / 30 GB testfile.
Single-peer matrix
90s ramp on a host-quiescent machine (no concurrent compiles), final-tick speed reported.
MaxDownload=0(unlimited)MaxDownload=20000(20 MB/s cap)MaxDownload=1000000(UI max, 1 GB/s)The cap is enforced to within 2.5% of the configured value across all three platforms, with no regression on the unlimited path. The
1000000(UI maximum) configured value is well above any of the three platforms' network ceilings, so the throttler doesn't artificially bottleneck — it just doesn't bind.Multi-peer redistribution
Two seeder amuled instances on the same host, sharing the same testfile-30gb.bin:
MaxUpload=300000(effectively unlimited)MaxUpload=5000(5 MB/s upload cap)Mac leech,
MaxDownload=20000(20 MB/s cap), 120s run. Per-seeder upload sampled mid-test:If the throttler did naive equal-split, both peers would be allocated 10 MB/s each; the slow peer is upload-capped at 5 MB/s and would only deliver 5, so the total would plateau at ~15 MB/s. We hit ~19.5 MB/s — the fast peer absorbs the slack the slow peer can't fill. Demand-aware redistribution confirmed.
Multi-file × multi-peer
Mac leech,
MaxDownload=20000, two files concurrently — testfile-5gb sourced from the slow seeder (MaxUpload=5000), testfile-30gb sourced from the fast seeder (unlimited). aMule UI readout at steady state:Three properties held simultaneously:
CDownloadBandwidthThrottler::Get()returns the function-local static); everyCEMSocket::OnReceive()consults the samem_bytesAvailableregardless of whichCPartFileit's sourcing.Note for context: aMule won't open two parallel connections from one leech to the same peer for different files (per-host connection rotation), which is why this test uses two distinct seeder instances. Same global cap behaviour would hold with N files × N peers regardless.
Notes for review
Reserve(0)returns 0 by construction;OnReceive()only consults the throttler whenreadMax > 0so empty-payload control packets still flow through the do-while loop's packet-completion path. (This was a real bug discovered during the rewrite — callingReserveunconditionally and treating0as "bucket exhausted" silently broke control-packet processing on the first iteration of the read loop, which manifested as connected peers showing0 MB/sindefinitely. Fixed by gating theReservecall onif (readMax).)<min>...<max>carry-over (2× budget) was the difference between a 10–17 MB/s noisy plateau and a 19.5 MB/s steady state on the same configured cap. Strict overwrite-on-refill starves TCP; permissive accumulation across many ticks would let a quiet period bank capacity that bursts well past the average. 2× budget is the smallest carry that smooths out the per-tick variance without inviting bursts that exceed the user's cap meaningfully.m_sendLockeralready serialisesRead()againstSend()per socket; addingReserve/Refundcalls inside the same critical section was deliberate so the per-socket bookkeeping (pendingOnReceive) and the global accounting can't race against a concurrentSend-side refund of upload budget.m_bytesAvailableisstd::atomic<int64_t>with CAS-based decrement;m_unlimitedisstd::atomic<bool>with acquire/release semantics.RefillBudgetruns on the wxApp main thread (timer event);Reserve/Refundrun on the asio I/O threads. No locks needed.