upload: make MaxUpload=0 mean literal unlimited - #461
Merged
mrjimenez merged 1 commit intoApr 24, 2026
Conversation
With MaxUpload=0 (the "unlimited" pref value) the throttler was setting allowedDataRate to the currently measured rate + 5 KB/s each iteration. This behaves as a hard cap that tracks the current rate: peer-side slow-start or a transient stall pins the rate low, which keeps allowedDataRate low, which prevents the rate from recovering. In practice the uplink ends up well below its real ceiling on any link faster than a few hundred KB/s. Three minimal changes: * common/Constants.h: add UNLIMITED_RATE = UINT_MAX sentinel so the throttle loop can distinguish "pref says unlimited" from "budget happens to be zero". * UploadBandwidthThrottler.cpp: in the UNLIMITED branch set allowedDataRate = UNLIMITED_RATE so the per-iteration cap is skipped entirely. Multiplying UINT_MAX by timeSinceLastLoop would overflow the sint32 bytesToSpend accumulator, so cap the budget rate at 1 GB/s for the bytesToSpend math — still far above any real uplink, and every socket send() short-circuits well below it. * UploadQueue.cpp: GetMaxSlots() in the UNLIMITED branch was nMaxSlots = currentRate/slotRate + 2, a chicken-and-egg trap that caps at 2 slots when observed uplink is 0 B/s — not enough parallel TCP flows to break cold-start and let the uplink ramp. Keep the speed-based formula but floor it at 20 slots. MAX_UP_CLIENTS_ALLOWED (250) still caps the upper end.
5 tasks
13 tasks
ngosang
added a commit
to ngosang/amule
that referenced
this pull request
Jul 13, 2026
…project#461) Clicking a download in the Downloads table opens a detail panel in the lower half of the page, separated from the table by a draggable splitter; the page fills the viewport and scrolls its regions internally (app-like) instead of scrolling the whole page. On phones the detail opens as a full-screen drill-down sheet over the list (below the top bar) with a pinned close button, so the tap clearly changes the screen and the info is reachable. The panel consumes the enriched GET /downloads/{hash} endpoint and updates live while open (re-fetches on each store tick; ETag-cached). Top of the panel: - Completion progress bar. - Canvas pieces graph mirroring the aMule GUI: green = complete, red = missing, blue = available with an intensity gradient by source count (theme-tuned via CSS vars). A compact one-line legend shows per-state counts, the total piece count, and the availability scale '(fewer -- more)'. - Copy ED2K link / Copy magnet link buttons on the file-name row. The magnet URI matches CamuleAppCommon::CreateMagnetLink; clipboard write falls back to execCommand on non-secure origins. Fields below, ordered by usefulness (status/progress first, technical last) and each with an explanatory tooltip: - Transfer: Status, Completed (bytes + %), Speed, Remaining, Sources, Size, Transferred. - Activity: Active time, Last received, Last seen complete, Clients on queue. - Media / Comment when present. - Parts: Available parts, Saved by ICH (in packages), Lost to corruption, Gained by compression. - Identity: Hash (uppercase), Met file. Also: add onRowClick to the shared VirtualTable, a 'copy' icon, widen the content cap to 1760px, move the Clients nav tab after Shared Files, and the en/es i18n strings.
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
When
MaxUploadis left at0(the user-facing "unlimited" value), aMule was not actually uploading uncapped. The throttler loop was settingallowedDataRateto the currently measured upload rate + 5 KB/s on every iteration:This behaves as a hard cap that tracks the current rate. Peer-side slow-start or a transient stall pins the rate low, which keeps
allowedDataRatelow, which prevents the rate from recovering. In practice the uplink ends up well below its real ceiling on any link faster than a few hundred KB/s.This PR makes
MaxUpload=0mean what the UI label says — no throttling at all, let the kernel and TCP congestion control do the rate-limiting. Same semantics as modern P2P applications.Why not auto-sense the uplink?
The leftover "+5 KB/s per iteration" was the vestige of an old UploadSpeedSense experiment. Before landing this minimal fix we attempted two richer auto-sense strategies on separate branches to see whether one could legitimately replace the static
UNLIMITED_RATEpath. Both were dropped:Attempt 1 —
upload-throttle-adaptive(eMule 0.70b port)Ported eMule 0.70b's
nSlotsBusyLevelsigned counter +CalculateChangeDeltaadaptive-step table (0.5% → 16% of current rate depending on how saturated the slot queue is). The design relies on eMule'sLastCommonRouteFinder— a ~900-line ICMP-based uplink probe that detects the uplink ceiling from the outside. aMule has never had a portable equivalent (no LCRF port to macOS/Windows), so the only feedback signal left was the observed send rate itself. With no independent probe, the loop overshoots the real uplink by 8-10× before multiplicative-decrease kicks in, saturates the uplink completely, and collapses downloads for the rest of the connection.Attempt 2 —
upload-tcp-info(per-socket RTT inflation probe)Per-socket TCP_INFO sample (Linux
getsockopt(TCP_INFO)via<linux/tcp.h>, macOSTCP_CONNECTION_INFO, WindowsWSAIoctl(SIO_TCP_INFO)). Comparedtcpi_min_rttbaseline against an EWMA-smoothed current RTT — AIMD-style: inflation beyondmax(baseline × 1.25, baseline + 5ms)→ back off, idle → additively ramp.Tightened nicely against
tc-shaped lab uplinks (convergence within a couple of seconds, steady ±5% of the shaped ceiling). But real-world testing exposed a fundamental problem: RTT inflation can't distinguish peer-side congestion from our own uplink saturation. A single slow leecher with a saturated downlink, or an ed2k server under load, both show the same RTT inflation pattern as our uplink saturating — and there are always some of those. The algorithm pulled the global estimate down on legitimate full-speed uploads.tcpi_sndbuf_limitedexists on recent Linux kernels and would in principle let us tell "I'm waiting on the send buffer" from "peer is slow", but it's Linux-only and not wired up on the macOS/Windows equivalents.Both branches are preserved in the fork for future work if someone finds a portable, peer-vs-uplink-disambiguating signal.
Decision
Modern P2P clients all treat
0as literal "no throttling" and let the kernel + TCP congestion control do the rate-limiting. That's the same uplink aMule shares with the rest of the network stack, so the kernel is in a better position to throttle it than any userspace heuristic we could write without an independent probe. Do the same here.Changes
Three minimal changes, 26 insertions / 5 deletions total.
src/include/common/Constants.hAdd an
UNLIMITED_RATE = UINT_MAXsentinel, distinct from the user-facingUNLIMITED = 0pref value, so the throttle loop can tell "pref says unlimited" from "budget is zero":src/UploadBandwidthThrottler.cppIn the
UNLIMITEDbranch ofEntry(), setallowedDataRate = UNLIMITED_RATEso the per-iteration cap is skipped entirely:Multiplying
UINT_MAXbytimeSinceLastLoopwould overflow thesint32bytesToSpendaccumulator, so cap the budget rate at 1 GB/s for thebytesToSpendmath — still far above any real uplink, and every socketsend()will short-circuit well below this ceiling:src/UploadQueue.cppGetMaxSlots()in theUNLIMITEDbranch previously computed:…which is a chicken-and-egg trap: with observed uplink at 0 B/s (fresh start, no peers connected yet) it caps at 2 slots, and 2 slots cannot carry enough parallel TCP flows to break cold-start and let the uplink ramp. Keep the speed-based formula but floor it at
N_FLOOR = 20so there is always enough concurrency for the ramp:MAX_UP_CLIENTS_ALLOWED(250) still caps the upper end.