Skip to content

ExternalConn: snapshot file lists to avoid O(N^2) GetFileByIndex loops (#666) - #687

Merged
mrjimenez merged 2 commits into
amule-project:masterfrom
got3nks:fix/external-conn-on2-getfilebyindex
May 23, 2026
Merged

ExternalConn: snapshot file lists to avoid O(N^2) GetFileByIndex loops (#666)#687
mrjimenez merged 2 commits into
amule-project:masterfrom
got3nks:fix/external-conn-on2-getfilebyindex

Conversation

@got3nks

@got3nks got3nks commented May 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #666.

Root cause

CSharedFileList::GetFileByIndex(i) walks the shared-file std::map via std::advance(begin(), i) under list_mut. The four EC handlers that drive amuleweb iterate with

for (uint32 i = 0; i < theApp->sharedfiles->GetFileCount(); ++i)
    cur_file = theApp->sharedfiles->GetFileByIndex(i);

which is O(N²) in walk cost and acquires list_mut 2N times. On @Stoatwblr's 43585-file install that is ~950M red-black-tree pointer-chases per refresh, with the main thread blocked the entire time. The asio event loop never gets to run → amuleweb sees the socket stop responding, retries, and amuled pegs a core for tens of minutes per request.

Confirmed by the gdb backtrace in #666:

#0 std::_Rb_tree_increment
#4 CSharedFileList::GetFileByIndex (index=43585) at SharedFileList.cpp:644
#5 Get_EC_Response_GetSharedFiles at ExternalConn.cpp:684
#6 CECServerSocket::ProcessRequest2 at ExternalConn.cpp:1486

Fix

Replace each of the four loops with a single CopyFileList() snapshot and iterate the resulting std::vector outside the lock:

  • Get_EC_Response_GetSharedFiles
  • Get_EC_Response_GetDownloadQueue
  • ECPartFileMsgSource ctor
  • ECKnownFileMsgSource ctor

The two download-queue variants were already O(N) (vector backing) but re-acquired m_mutex on every iteration; they get the same cleanup for free.

Freshness

From the EC client's perspective freshness is unchanged — every request already represents a single point-in-time view. The snapshot just makes that view internally consistent (no inserts/removes smeared across N iterations) and drops the wall-clock from minutes to milliseconds. The CopyFileList() helpers already exist on both lists (SharedFileList.cpp:691, DownloadQueue.cpp:181).

Not in scope

Three other call sites use the same pattern but are O(N) only (vector backing) and outside the EC main-thread path: BaseClient::SendSharedFilesOfDirectory, four CDownloadQueue::Process/Save/Load internal loops, and CUploadQueue::Process.

Test

macOS local build of amule amuled amulegui amuleweb passes. @Stoatwblr — could you cherry-pick fix/external-conn-on2-getfilebyindex (or rebuild master with this commit applied) and confirm amuleweb stops wedging?

CSharedFileList::GetFileByIndex(i) does a std::advance() of i steps over
the underlying std::map and acquires list_mut on every call, making the
classic

    for (i = 0; i < GetFileCount(); ++i) GetFileByIndex(i)

loop O(N^2) in both walk-cost and lock-acquire count. For users with
tens of thousands of shared files this pegs the main thread for tens of
minutes per EC request, blocking asio and wedging amuleweb at 100% CPU
(issue amule-project#666: 43585 shared files, ~950M iterations per refresh).

Replace the four EC hot-path loops with a single CopyFileList()
snapshot, then iterate the resulting vector outside the lock:

  * Get_EC_Response_GetSharedFiles
  * Get_EC_Response_GetDownloadQueue
  * ECPartFileMsgSource::ECPartFileMsgSource (ctor)
  * ECKnownFileMsgSource::ECKnownFileMsgSource (ctor)

The downloadqueue variants were already O(N) but re-acquired m_mutex
on every iteration; they get the same cleanup for free.

Freshness from the EC client's perspective is unchanged -- each request
already represents a single point-in-time view, the snapshot just makes
that view internally consistent (no inserts/removes smeared across the
N iterations) and reduces wall-clock from minutes to milliseconds.

CopyFileList() helpers already exist on both CSharedFileList
(SharedFileList.cpp:691) and CDownloadQueue (DownloadQueue.cpp:181).
@got3nks got3nks mentioned this pull request May 23, 2026
@mrjimenez
mrjimenez merged commit c404cbc into amule-project:master May 23, 2026
12 checks passed
@got3nks
got3nks deleted the fix/external-conn-on2-getfilebyindex branch May 23, 2026 20:06
ngosang added a commit to ngosang/amule that referenced this pull request Jul 30, 2026
…mule-project#687) (amule-project#719)

1bc9d6e (amule-project#694) added GET /flags/{code}.png, but nothing consumed it: the
country column of the peer list and of the ed2k server list still painted only
the bare uppercase code, duplicating the same one-liner cell in two views.

Both now render through a single new CountryCell in components.js: the 16x11
famfamfam flag from the route followed by the code, the layout the desktop
lists already use. The image URL is built relative to window.location.pathname
like BASE in api.js, so it survives a reverse-proxy subpath, and the route's
one-day Cache-Control means a peer list full of <img> tags doesn't re-fetch a
flag per country on reload.

country_code is an empty string when the daemon's GeoIP is off or the IP
doesn't resolve (amule-project#439, amule-project#440), so an empty code short-circuits to the dash the
cell already showed -- no <img>, and in particular no request for the
"/flags/.png" the route would 404. A well-formed code the flag set has no
artwork for (zz, GeoIP pseudo-codes like ap/eu) also 404s, so onError hides
the image and leaves the code readable instead of a broken-image icon.

The cell gains a title with the localized country name via Intl.DisplayNames
({ type: "region" }) in the UI language, as docs/api/REFERENCE.md prescribes:
no endpoint and no new translation keys, since the browser already has the
data. The formatter is built once per module load, not once per row.

Column width goes 52px -> 70px to fit flag + code + sort arrow; sortVal is
unchanged, so the column still sorts by code.

Verified against a live daemon: flags render in the Networks server list and
the Clients list, an empty country_code shows the dash alone, /flags/zz.png
falls back to the hidden image, the tooltip follows the EN/ES switch
(France/Francia), sorting works both directions, and the console stays clean.
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.

AMuleweb scaling issues

2 participants