Skip to content

upload: make MaxUpload=0 mean literal unlimited - #461

Merged
mrjimenez merged 1 commit into
amule-project:masterfrom
got3nks:upload-maxupload-unlimited
Apr 24, 2026
Merged

upload: make MaxUpload=0 mean literal unlimited#461
mrjimenez merged 1 commit into
amule-project:masterfrom
got3nks:upload-maxupload-unlimited

Conversation

@got3nks

@got3nks got3nks commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

When MaxUpload is left at 0 (the user-facing "unlimited" value), aMule was not actually uploading uncapped. The throttler loop was setting allowedDataRate to the currently measured upload rate + 5 KB/s on every iteration:

// Before
if (thePrefs::GetMaxUpload() == UNLIMITED) {
    allowedDataRate = (uint32)theStats::GetUploadRate() + 5 * 1024;
}

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.

This PR makes MaxUpload=0 mean 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_RATE path. Both were dropped:

Attempt 1 — upload-throttle-adaptive (eMule 0.70b port)

Ported eMule 0.70b's nSlotsBusyLevel signed counter + CalculateChangeDelta adaptive-step table (0.5% → 16% of current rate depending on how saturated the slot queue is). The design relies on eMule's LastCommonRouteFinder — 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>, macOS TCP_CONNECTION_INFO, Windows WSAIoctl(SIO_TCP_INFO)). Compared tcpi_min_rtt baseline against an EWMA-smoothed current RTT — AIMD-style: inflation beyond max(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_limited exists 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 0 as 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.h

Add an UNLIMITED_RATE = UINT_MAX sentinel, distinct from the user-facing UNLIMITED = 0 pref value, so the throttle loop can tell "pref says unlimited" from "budget is zero":

const unsigned UNLIMITED = 0;

// Internal sentinel for "no upload throttling at all" — used when the user
// has set MaxUpload=0 in prefs. Distinct from UNLIMITED=0 (the user-facing
// pref value) so the throttle loop can skip the per-iteration rate cap math
// entirely rather than dividing a budget by zero or by a meaningless ramp.
#include <climits>
const unsigned UNLIMITED_RATE = UINT_MAX;

src/UploadBandwidthThrottler.cpp

In the UNLIMITED branch of Entry(), set allowedDataRate = UNLIMITED_RATE so the per-iteration cap is skipped entirely:

if (thePrefs::GetMaxUpload() == UNLIMITED) {
    // MaxUpload=0 means literal unlimited — bypass the per-iteration rate cap
    // so SendFileAndControlData() is never throttled.
    allowedDataRate = UNLIMITED_RATE;
} else {
    allowedDataRate = thePrefs::GetMaxUpload() * 1024;
}

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() will short-circuit well below this ceiling:

const uint32 bytesToSpendRate = (allowedDataRate == UNLIMITED_RATE)
    ? (1024u * 1024u * 1024u)
    : allowedDataRate;
bytesToSpend += (sint32) (bytesToSpendRate / 1000.0 * timeSinceLastLoop);

src/UploadQueue.cpp

GetMaxSlots() in the UNLIMITED branch previously computed:

nMaxSlots = (uint32)(kBpsUp / kBpsUpPerClient) + 2;

…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 = 20 so there is always enough concurrency for the ramp:

const uint32 N_FLOOR = 20;
float kBpsUp = theStats::GetUploadRate() / 1024.0f;
uint32 bySpeed = (uint32)(kBpsUp / kBpsUpPerClient) + 2;
nMaxSlots = bySpeed > N_FLOOR ? bySpeed : N_FLOOR;

MAX_UP_CLIENTS_ALLOWED (250) still caps the upper end.

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.
@mrjimenez
mrjimenez merged commit f6c820b into amule-project:master Apr 24, 2026
5 checks passed
@got3nks
got3nks deleted the upload-maxupload-unlimited branch May 3, 2026 15:20
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.
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.

2 participants