Skip to content

feat(gui): show free disk space on the Downloads and Shared Files panels (#757) - #847

Merged
got3nks merged 2 commits into
amule-org:masterfrom
got3nks:feat/free-space-field
Aug 7, 2026
Merged

feat(gui): show free disk space on the Downloads and Shared Files panels (#757)#847
got3nks merged 2 commits into
amule-org:masterfrom
got3nks:feat/free-space-field

Conversation

@got3nks

@got3nks got3nks commented Aug 7, 2026

Copy link
Copy Markdown

Fixes #757.

The Downloads panel now shows the free space on the filesystem holding the part files next to the queue size, red once that space no longer covers what is left to download. The Shared Files panel shows the free space where finished downloads land.

Which filesystem, and what to compare against

Both follow from how the directories actually work, and this is where the design decisions are:

Downloads reports temp, and the warning is measured against the whole queue. There is one temp directory for every category — a category chooses where a file lands when it finishes, not where it downloads — so the figure is inherently global. The displayed queue size stays category-filtered as before, but the red state is driven by every queued file, because they all compete for the same space. Filtering the comparison by category would only warn once it was already too late.

The threshold uses the bytes still to download, not the total queue size. The bytes a part file already holds are off the free-space figure already, so what remains is exactly what the disk still has to find room for. Comparing against the total would warn far earlier than necessary, and more so with sparse part files.

Shared Files reports the default category's incoming, with no threshold. Incoming is per category (Category_Struct::path) and that panel has no category selector, so there is no single figure covering all of them. Nothing there stops when the disk fills, so a warning colour would have nothing to mean.

Getting the figures to the GUI

Only the core can answer either question: the GUI may be on another machine entirely, and even where it mounts the same share it can see a different size or quota. Two new stats tags carry the figures over EC.

An absent tag means unknown, not zero — a daemon older than these tags leaves the field empty rather than reporting a full disk and painting it red.

Both getters are cache-backed and re-sample at most every ten seconds. statvfs() / GetDiskFreeSpaceEx() blocks on the directory, and on a network mount — temp and incoming commonly are one — a slow or stale server blocks it for as long as the mount's timeout. The callers are a per-second GUI refresh plus every stats poll from every connected client, so an uncached read would multiply that exposure by the poll rate. This doesn't introduce a new failure mode (aMule already reads and writes every part file on that mount) — it just avoids re-entering a blocking call at the poll rate.

A path that cannot be queried reports FREE_SPACE_UNKNOWN, which empties the label instead of printing "0 bytes", and never colours it red.

Layout

The Shared Files statistics box goes from four columns to five: the collapse/expand button keeps a narrow column of its own, and the size figures get a column beside it with one figure per row — so the total size lines up with the counters and the free space with the gauges.

Strings

One new translatable string, "Free space: %s", shared by both panels; catalogs regenerated. Nothing was removed or changed — no existing translation was touched. msgmerge pre-filled 32 catalogs with a #, fuzzy guess matched from "Server message: %s"; fuzzy entries are excluded by msgfmt, so those languages show the English until a translator confirms it. The separator between the queue size and the free space is built in code, so translators are given the figure alone.

Verification

Builds clean on macOS (amule + amulegui + amuled), no warnings from project sources. clang-format and both clang-tidy tiers clean over the diff. Catalogs verified idempotent against a second regen.

Tested interactively on macOS, monolithic and amulegui against a local daemon: both figures, the red threshold, and the panel layouts. Linux and Windows have not had a visual pass yet — the layout change is in shared code, so the Shared Files panel is worth a look on both.

The Downloads panel now reports the free space on the filesystem holding
the part files beside the queue size, and turns red once that space no
longer covers what is left to download. The Shared Files panel reports
the free space where finished downloads land. Requested in amule-project#757.

Which filesystem, and what to compare against, both follow from how the
directories actually work. There is one temp directory for every
category -- a category chooses where a file lands when it finishes, not
where it downloads -- so the figure is global and the warning is
measured against the whole queue rather than the category on screen; a
per-category comparison would only fire once it was already too late.
The threshold uses the bytes still to download, not the total queue
size: the bytes a part file already holds are off the free-space figure
already, so what is left is exactly what the disk still has to find room
for. Incoming, by contrast, is per category, and the Shared Files panel
has no category selector, so it reports the default category's -- and
carries no threshold, since nothing there stops when the disk fills.

Only the core can answer either question: the GUI may be on another
machine entirely, and even where it mounts the same share it can see a
different size or quota. Two new stats tags carry the figures over EC.
An absent tag means unknown rather than zero, so a daemon older than
these tags leaves the field empty instead of reporting a full disk.

Both getters are cache-backed, re-sampling at most every ten seconds.
statvfs()/GetDiskFreeSpaceEx() blocks on the directory, and on a network
mount -- temp and incoming commonly are one -- a slow or stale server
blocks it for as long as the mount's timeout. The callers are a
per-second GUI refresh and every stats poll from every connected client,
so an uncached read would multiply that exposure by the poll rate. A
path that cannot be queried at all reports FREE_SPACE_UNKNOWN, which
empties the label rather than printing "0 bytes" and never colours it
red.

The GUI side is gated on what is actually on screen: the timer refreshes
only the panel currently displayed, and nothing at all while the window
is hidden to the tray or minimized. That matters because the Downloads
refresh walks the whole queue to decide whether to warn -- skipped
outright when there is no figure to compare against -- and because each
label is resolved by name, which is now done once and cached rather than
per tick.

The Shared Files statistics box grows from four columns to five: the
collapse/expand button keeps a narrow column of its own and the size
figures get a column beside it, one figure per row, so the total size
lines up with the counters and the free space with the gauges.

One new translatable string, "Free space: %s", shared by both panels;
catalogs regenerated. The separator between the queue size and the free
space is built in code so translators are given the figure alone.

Builds clean on macOS (amule + amulegui + amuled); clang-format and both
clang-tidy tiers clean over the diff.
@got3nks
got3nks force-pushed the feat/free-space-field branch from 61e758a to e24f42f Compare August 7, 2026 15:38
CStatistics::GetTempFreeSpace() / GetIncomingFreeSpace() called
CPath::GetFreeSpaceAt() inline, cached for ten seconds. Both callers are
latency critical: the GUI timer on the main thread, and the EC stats
reply on amuled's core thread.

statvfs() / GetDiskFreeSpaceEx() blocks on the directory it is asked
about. Measured read-only on a live host, 50 samples per path: ext4 0.002
ms median, mergerfs 0.678 ms, nfs4 0.204-0.222 ms, worst case 1.765 ms.
So the healthy cost was never the problem, and the interval was not
either. What those numbers do not cover is the pathological case: a cold
autofs mount takes tens to hundreds of milliseconds, and an unreachable
server blocks for timeo x retrans on a soft mount and indefinitely on a
hard one. Either directory is commonly a network mount, and that stall
landed on the main loop -- freezing the GUI and stalling EC replies.

Nothing else in the core reaches those filesystems from the main loop:
downloads write through CPartFileWriteThread, uploads read through
CUploadDiskIOThread. This probe was the one exception.

CFreeSpaceThread is a dedicated joinable worker rather than a task on
CThreadScheduler, for the reason CMediaProbeThread was split out (amule-project#280):
the scheduler runs one task at a time and owns completion, allocation,
hashing, verification and IP filtering, so a probe blocked on a hung
mount would not delay that queue but stop it -- and would couple
unrelated filesystems, a hung incoming mount blocking the hashing of part
files on a healthy local disk. Here a hung mount can only ever delay the
next sample: the figure goes stale, the label empties, nothing else
notices.

The two figures become std::atomic<sint64>, written by the worker and
read by the main and EC threads; the getters are plain relaxed loads that
cannot block, so no caller changed. The worker wakes every second and
samples each path only when its own interval has elapsed -- separately
timed, so a slow incoming cannot hold temp back -- and the wake interval
is shorter than the sample interval so shutdown does not wait out a
sleeping thread.

The worker reads no preferences. The temp and incoming paths can change
under the preferences dialog, no worker in the tree touches thePrefs, and
a wxString read while another thread assigns it is a race whatever the
value; so CamuleApp hands the worker mutex-guarded copies, at
construction and on each core tick. That also removes any question of
teardown ordering against thePrefs.

Constructed alongside mediaProbeThread, which is post-fork so the POSIX
threads belong to the daemon child (amule-project#849), and torn down beside it in
OnExit().

The panel-visibility gate stays, but it now skips pointless work rather
than a blocking call: CDownloadListCtrl::UpdateFreeSpace() still walks
the queue to decide whether to warn. Refreshing when a panel becomes
active as well as on the timer means switching to a panel no longer
shows the figure it had when it was last visible until the next tick.
@got3nks

got3nks commented Aug 7, 2026

Copy link
Copy Markdown
Author

Pushed 4224002: the free-space probe now runs on its own thread instead of inline in the getters.

CPath::GetFreeSpaceAt() was called from CStatistics::GetTempFreeSpace() / GetIncomingFreeSpace(), cached for ten seconds — so from the GUI timer on the main thread, and from the EC stats reply on amuled's core thread. Measured read-only on a live host, 50 samples per path, the healthy cost is nothing to worry about:

path type median max
/ ext4 0.002 ms 0.002 ms
/mnt/nas-merged mergerfs 0.678 ms 1.765 ms
/mnt/autofs/nas-data nfs4 0.204 ms 0.309 ms
/mnt/autofs/nas nfs4 0.222 ms 0.750 ms

The interval was never the issue. What those numbers don't cover is the case that matters: a cold autofs mount is tens to hundreds of milliseconds, and an unreachable server blocks for timeo×retrans on a soft mount and indefinitely on a hard one. Either directory is commonly a network mount, so that stall was landing on the main loop — freezing the GUI and stalling EC replies. Worth noting nothing else in the core touches those filesystems from the main loop: downloads write via CPartFileWriteThread, uploads read via CUploadDiskIOThread. This probe was the exception.

CFreeSpaceThread is a dedicated worker rather than a CThreadScheduler task, for the same reason CMediaProbeThread was split out in #280: that scheduler runs one task at a time and owns completion, allocation, hashing, verification and IP filtering, so a probe blocked on a hung mount wouldn't delay that queue, it would stop it — and it would couple unrelated filesystems, with a hung incoming mount blocking the hashing of part files on a healthy local disk. On its own thread, a hung mount can only delay the next sample: the figure goes stale, the label empties, nothing else notices.

The figures are now std::atomic<sint64> and the getters are relaxed loads that cannot block, so no caller changed. Each path is timed separately, so a slow incoming can't hold temp back.

One thing not in the brief: the worker reads no preferences. The paths change under the preferences dialog, no worker in the tree touches thePrefs, and a wxString read while another thread assigns it is a race regardless of the value — so CamuleApp hands the worker mutex-guarded copies at construction and on each core tick. That also settles teardown ordering against thePrefs by construction.

The panel-visibility gate stays, but now it skips pointless work rather than a blocking call — CDownloadListCtrl::UpdateFreeSpace() still walks the queue to decide whether to warn. It also refreshes when a panel becomes active, so switching no longer shows the previous figure until the next tick.

Rebuilt and retested on macOS: amule, amulegui and amuled, clang-format and both clang-tidy tiers clean.

@got3nks
got3nks merged commit e125eae into amule-org:master Aug 7, 2026
14 checks passed
@got3nks
got3nks deleted the feat/free-space-field branch August 7, 2026 16:58
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.

Add "Free space" field to downloads + shared files panel

1 participant