Skip to content

fix(gui): rebuild the download list in one pass when filtering or switching category - #670

Merged
got3nks merged 1 commit into
amule-org:masterfrom
got3nks:fix/download-filter-bulk-rebuild
Jul 28, 2026
Merged

fix(gui): rebuild the download list in one pass when filtering or switching category#670
got3nks merged 1 commit into
amule-org:masterfrom
got3nks:fix/download-filter-bulk-rebuild

Conversation

@got3nks

@got3nks got3nks commented Jul 28, 2026

Copy link
Copy Markdown

Fixes #669.

Typing in the transfer-window text filter froze the GUI for seconds on a large queue — @ghysler reports ~10 s per keystroke with 10k downloads, with keystrokes stacking up. The shared-files filter is instant at the same scale.

Root cause

SetFilterText() re-evaluated every model item and called ShowFile(file, visible) for each. Every row that had to disappear went through RemoveItemData(), which does a full RebuildRowIndex() plus a RefreshFromRow()O(n) per removal, so O(n²) for one keystroke. The shared-files filter is fast because it does the opposite: clears the model, re-appends the passing items, and finishes with a single FinishBulkLoad().

ChangeCategory() had the identical shape, so switching category on a large queue was equally slow. That wasn't reported, but it's the same bug and is fixed here.

Fix

RebuildVisibleList() clears, appends every item passing the current category + text filter, and finishes with one FinishBulkLoad() (one SetItemCount, one index rebuild, one sort) — O(n log n) and a single repaint. ShowFileList(), the filter and ChangeCategory() all route through it, so the three paths no longer duplicate the visibility loop. ShowFilesCount(diff) delegates to a new SetFilesCount(count) so the rebuild can set the count absolutely without duplicating the label update.

De-duplicating the filter

The two lists had grown a filter each: identical m_filterText members, identical SetFilterText() bodies, and byte-identical PassesTextFilter() implementations differing only in the parameter type (const CPartFile* vs const CKnownFile*).

That now lives once in CMuleVirtualListCtrlm_filterText, SetFilterText() and a string-based MatchesFilter(), plus a RebuildFilteredView() hook each list overrides with its own rebuild. MatchesFilter() takes a name rather than a file so the generic control keeps knowing nothing about what its rows represent, and each subclass is left owning only what is genuinely specific: which model it walks and what it matches on. (CSearchListCtrl's filter is a regex on the non-virtual base and is untouched.)

Selection preservation

A clear-and-re-append rebuild renumbers every row, and the virtual control tracks selection and focus by row index — so both would be dropped, where the incremental path kept them for free by leaving surviving rows untouched.

CMuleVirtualListCtrl grows SaveSelection() / RestoreSelection() for this. SortList() already did the same save-and-reapply inline and now shares them, and so does CSharedFilesCtrl::ShowFileList() — which fixes a pre-existing bug of its own: editing the shared-files filter dropped the selection.

Testing

Built amule and amulegui clean. The algorithmic change is the substance here; a 10k-download reproduction wasn't available locally, so a real-world check on the reporter's setup is welcome before this is relied on.

…tching category

Typing in the transfer-window text filter froze the GUI for seconds on a
large queue -- ~10 s per keystroke with 10k downloads, with keystrokes
stacking up (issue amule-project#669).

SetFilterText() re-evaluated every model item and called ShowFile(file,
visible) for each one. Every row that had to disappear went through
RemoveItemData(), which does a full RebuildRowIndex() plus a
RefreshFromRow() -- O(n) per removal, so O(n^2) for the pass. The
shared-files filter is instant because it does the opposite: it clears the
model and re-appends the passing items, finishing with a single
FinishBulkLoad().

Give the download list the same shape. RebuildVisibleList() clears, appends
every item passing the current category + text filter, and finishes with one
FinishBulkLoad() (one SetItemCount, one index rebuild, one sort) -- O(n log n)
and a single repaint. ShowFileList(), the filter and ChangeCategory() all
route through it, so the identical O(n^2) in the category switch is fixed too
and the three paths no longer duplicate the visibility loop.
ShowFilesCount(diff) now delegates to SetFilesCount(count) so the rebuild can
set the count absolutely without duplicating the label update.

The two lists had also grown a filter each: identical m_filterText members,
identical SetFilterText() bodies and byte-identical PassesTextFilter()
implementations differing only in the parameter type. That now lives once in
CMuleVirtualListCtrl -- m_filterText, SetFilterText() and a string-based
MatchesFilter(), plus a RebuildFilteredView() hook each list overrides with
its own rebuild. The base stays agnostic about what its rows represent.

A clear-and-re-append rebuild renumbers every row, and the virtual control
tracks selection and focus by row index, so both would be lost -- the
incremental path kept them for free by leaving surviving rows untouched.
CMuleVirtualListCtrl grows SaveSelection()/RestoreSelection() for this;
SortList() already did the same save-and-reapply inline and now shares them,
as does CSharedFilesCtrl::ShowFileList(), which fixes the pre-existing loss
of selection when editing the shared-files filter.
@got3nks
got3nks force-pushed the fix/download-filter-bulk-rebuild branch from 935d15d to 5fd29bb Compare July 28, 2026 13:05
@got3nks
got3nks merged commit 2b85d91 into amule-org:master Jul 28, 2026
13 checks passed
@got3nks
got3nks deleted the fix/download-filter-bulk-rebuild branch July 28, 2026 14:29
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.

Text filter rebuilds download table on every insert

1 participant