Skip to content

perf(ec): skip unchanged search results on the multi-search union poll - #723

Merged
got3nks merged 2 commits into
amule-org:masterfrom
got3nks:perf/search-union-partial-update
Jul 30, 2026
Merged

perf(ec): skip unchanged search results on the multi-search union poll#723
got3nks merged 2 commits into
amule-org:masterfrom
got3nks:perf/search-union-partial-update

Conversation

@got3nks

@got3nks got3nks commented Jul 30, 2026

Copy link
Copy Markdown

An idle amuleGUI sitting on the Search panel spends roughly 14 KB per poll for every 1000 search results it holds, purely to tell the daemon that nothing has changed. At the 3 s cadence that is about 17 MB/hour per 1000 results, per connected client — and it scales with how much the user has open, not with how much changed.

Every figure below was measured with amule-ec-profiler, a pass-through EC proxy that sizes each packet and breaks it down per tag, so the costs here are observed on the wire rather than reasoned from the source.

What it costs today

Measured against a daemon holding two finished searches and ~900 results, with the search tab open and nothing running:

tag per poll per result share
EC_TAG_SEARCHFILE 7.0 KB ~7.8 B 57%
EC_TAG_SEARCH_ID 5.3 KB ~5.9 B 43%
total 12.3 KB ~13.7 B

Nothing else — every mutable field diffs away correctly through the per-connection valuemap. What remains is a fixed ~14 bytes per result, per poll, forever, for results nobody is looking at.

Why both halves were there

The envelope was load-bearing. CRemoteContainer::ProcessUpdate deletes any item absent from the reply, so omitting an unchanged result made the client drop it from the tab. Absence was the deletion protocol.

EC_TAG_SEARCH_ID was then emitted unconditionally, outside the valuemap, so a result re-created after such a deletion could still be attributed to a tab. It was guarding against a case the first half caused.

Both fall away once removal is explicit.

The change

The union now skips a result whose valuemap diff is empty and that the client already has, emits EC_TAG_FILE_REMOVED for results the core no longer holds, and sends EC_TAG_SEARCH_ID only on a result's first appearance. CSearchListRem::ProcessUpdate deletes only on those tombstones instead of sweeping by absence. A tombstone also clears that ECID's valuemap, so a re-created result cannot inherit a diff that would strip the fields it needs — the same hazard freshEcids guards on the file path.

This reuses the existing knownfile tombstone rather than inventing a second one: the meaning ("this ECID is gone") and the payload are identical.

Compatibility

Negotiated as its own capability, EC_TAG_CAN_PARTIAL_SEARCH, not by reusing EC_TAG_CAN_PARTIAL_UPDATE. That existing flag is advertised unconditionally by every RemoteConnect client and means the file INC_UPDATE stream. An already-released amuleGUI advertises it, speaks multi-search, and still deletes any result missing from the reply — so reusing it would make such a client silently lose its search results against a newer daemon.

daemon client behaviour
new new amuleGUI skips unchanged, explicit tombstones
new older amuleGUI tag absent → union re-sends everything, exactly as today
old new amuleGUI no AUTH_OK echo → client keeps the absence sweep
any amuleweb / amuleapi / amulecmd never reach the union

amuleweb never sets EC_TAG_CAN_MULTI_SEARCH, so it stays on the per-search EC_DETAIL_UPDATE branch; amuleapi addresses searches by id at EC_DETAIL_FULL.

Measured

Same daemon, same searches, before and after:

master this branch
results held 827 1491
envelope + search id per poll 11,616 B 0 B
per result per poll ~14 B 0 B
total per poll (idle) ~11.6 KB 10 B
projected at 1000 results ~14 KB/poll 10 B/poll
EC_TAG_SEARCH_ID every result, every poll first appearance only

The two runs held different amounts, which is the point: master's cost is per result, so it has to be compared per result. This branch's 10 bytes is a bare envelope with a zero tag count — a constant, unchanged whether the client holds 800 results or 1500. The cost is now proportional to what changed, not to what is open.

Mechanism confirmed on the wire rather than inferred: closing two tabs produced 1223 EC_TAG_FILE_REMOVED tombstones once each; a subsequent search delivered 313 EC_TAG_SEARCHFILE against 269 EC_TAG_SEARCH_ID, the 44-tag gap being updates to already-known results correctly omitting the id.

Testing

  • amuleGUI against a live daemon on ed2k + Kad: four searches started, two tabs closed, a fourth search run. Results stay put, close removes them, new results land in the right tab.
  • The same daemon with an amuleGUI built from 70c1ec09a (this branch's base, so the only difference is this change): 11,616 B per poll of envelope + id, zero tombstones, results intact. The fallback is per connection, not global.
  • amuleapi curl suite 32/32 phases, 19-search.sh 108/108.
  • clang-format v18 and Tier-2 clang-tidy on changed lines, both clean.

Two things stated rather than measured, so they can be challenged cheaply:

  • Old daemon, new client is argued, not run. m_serverPartialSearch has exactly two writes — false in the constructor and true only when the daemon echoes the capability in AUTH_OK — so against an old daemon the client falls through to the unmodified base ProcessUpdate. The premise that an old daemon ignores an unknown AUTH_REQ tag is the same mechanism ten existing capability tags already rely on.
  • Kad comments are unaffected by construction: that block bypasses the valuemap, so whenever there is anything to report the tag has children and the skip cannot fire. The one case it does not cover — a Kad comment search that finishes finding nothing never telling the client it ended — behaves identically on master, since the client only acts on a tag that is present.

Results carrying Kad comments are still re-sent in full every poll, because that block has no valuemap. That is unchanged here, but it will now be the only remaining per-poll cost and is worth a separate look.

got3nks added 2 commits July 30, 2026 23:16
The EC_DETAIL_INC_UPDATE union re-sent one tag per live result on every
poll, for every open search, whether or not anything had changed. Measured
against a daemon with two finished searches and ~900 results, that is 12.3 KB
every 3 s of which not one field is new: 7.0 KB of EC_TAG_SEARCHFILE
envelopes and 5.3 KB of EC_TAG_SEARCH_ID.

The envelopes were load-bearing. CRemoteContainer::ProcessUpdate deletes any
item absent from the reply, so omitting an unchanged result made the client
drop it from the tab. EC_TAG_SEARCH_ID was then sent unconditionally, outside
the valuemap, so a result re-created after such a deletion could still be
attributed to a tab.

Both fall away once removal is explicit. The daemon now skips a result whose
valuemap diff is empty, emits EC_TAG_FILE_REMOVED for results the core no
longer holds, and sends EC_TAG_SEARCH_ID only on a result's first appearance.
CSearchListRem::ProcessUpdate deletes only on those tombstones instead of
sweeping by absence. An idle search costs nothing.

Negotiated as its own capability, EC_TAG_CAN_PARTIAL_SEARCH, rather than
reusing EC_TAG_CAN_PARTIAL_UPDATE. That existing flag is advertised by every
RemoteConnect client and means the *file* INC_UPDATE stream; an already
released amuleGUI advertises it, speaks multi-search, and still deletes any
result missing from the reply, so reusing it would make such a client
silently lose its search results against a newer daemon. amuleweb and
amuleapi never reach the union at all (no EC_TAG_CAN_MULTI_SEARCH, and
EC_DETAIL_FULL per id respectively).

A tombstone also clears that ECID's valuemap, so a re-created result cannot
inherit a diff that would strip the fields it needs.

Not yet measured end to end.
clang-format v18 rewrapped Get_EC_Response_Search_Results_Union's parameter
list; Tier-2 clang-tidy flagged the new EC_TAG_FILE_REMOVED walk in
CSearchListRem::ProcessUpdate as modernize-loop-convert.
@got3nks
got3nks merged commit afe5afb into amule-org:master Jul 30, 2026
15 checks passed
@got3nks
got3nks deleted the perf/search-union-partial-update branch July 30, 2026 21:58
LSalami added a commit to LSalami/amule that referenced this pull request Jul 31, 2026
muleToolbar() duplicated several msgid source-location references
(Networks, Searches, Downloads Window, etc.) already present via
amuleDlg.cpp's own toolbar setup. Deleting it drops those now-stale
#: comments; no msgid added, removed, or retranslated -- verified via
unchanged msgid count (1903 before/after) and a diff limited to
source-location comments and POT-Creation-Date.

Regenerated after rebasing onto upstream/master to pick up po/
changes from amule-org#723/amule-org#724/amule-org#726/amule-org#728/amule-org#730/amule-org#731, which had drifted our
prior regeneration out of sync.
got3nks pushed a commit that referenced this pull request Jul 31, 2026
…r.cpp (#675) (#725)

* chore(gui): delete unreachable bitmap functions/entries from muuli_wdr.cpp

First slice of the icon-system cleanup scoped in #675: remove code with
zero call sites anywhere in the tree, before any wxArtProvider migration
work starts.

- muleToolbar(): whole function unused -- superseded by the main
  wxToolBar setup in amuleDlg.cpp; nothing calls it.
- moreImages(): whole function unused, both of its two icon entries.
- amuleDlgImages(): 21 of 35 index blocks have no caller anywhere
  (0-13, 16, 17, 19, 27, 28, 31, 34). The 14 live ones are untouched --
  10 of those (20-26, 29, 32, 33) are already the fallback path inside
  amuleDlg.cpp's Add_Skin_Icon, which prefers a wxArtProvider/SVG lookup
  first; the other 4 (14, 15, 18, 30) are still called directly.
- amuleSpecial(): 6 of 26 index blocks have no caller (6, 7, 8, 9, 18,
  20) -- checked both direct call sites and the PrefsUnifiedDlg.cpp
  fallback table (pages[].m_imageidx), which uses neither.

convert_xpm in PartFileConvertDlg.cpp was on the same "no literal
grep hits" list initially but is not actually dead -- SetIcon(wxICON
(convert)) reaches it via the wxICON macro's token-pasting
(X##_xpm), invisible to a plain identifier search. Caught by a full
build failing on the undeclared identifier, not by inspection; left
untouched.

Deletes 1373 lines (~16% of the file). No behavior change: every
touched entry was unreachable code. clang-format v18 clean; full
amule build verified (macOS, CLIENT_GUI unaffected since neither
touched symbol is CLIENT_GUI-only).

* chore(po): regenerate catalogs after muuli_wdr.cpp dead-code removal

muleToolbar() duplicated several msgid source-location references
(Networks, Searches, Downloads Window, etc.) already present via
amuleDlg.cpp's own toolbar setup. Deleting it drops those now-stale
#: comments; no msgid added, removed, or retranslated -- verified via
unchanged msgid count (1903 before/after) and a diff limited to
source-location comments and POT-Creation-Date.

Regenerated after rebasing onto upstream/master to pick up po/
changes from #723/#724/#726/#728/#730/#731, which had drifted our
prior regeneration out of sync.
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.

1 participant