perf(gui): clear DeviceList in one pass and fix discovery timer lifecycle - #3316
Open
geraldo-netto wants to merge 4 commits into
Open
perf(gui): clear DeviceList in one pass and fix discovery timer lifecycle#3316geraldo-netto wants to merge 4 commits into
geraldo-netto wants to merge 4 commits into
Conversation
rob-4: discover_devices started a GLib timeout for update_progress but discarded the source id, so stop_discovery() left it running until the callback next observed discovering=False. Store the id, remove it in stop_discovery via _remove_discovery_timeout, and null it on the callback's own exit paths so a self-ending timer is never double-removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
perf-3: clear() looped over the liststore calling device_remove_event for each row -- every GenericList.delete() calls the O(n) Gtk.ListStore.iter_is_valid, making the clear O(n^2), and it mutated the liststore while iterating it before calling liststore.clear() anyway. Replace the loop with a single liststore.clear(). Fixes the iterate-while-mutating bug and drops the redundant pass. Adds test/benchmarks/bench_devicelist_clear.py comparing the original, perf-3, and vec-3 strategies, and tests covering clear() on empty/non-empty stores, reference release, and a size fuzz. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vec-3: clearing the liststore while path_to_row still held a live Gtk.TreeRowReference per row forced GTK to update every reference as rows were removed -- ~O(n^2). Reset path_to_row first so clear() has no live references to keep in sync. Benchmark (bench_devicelist_clear.py): ~4.3x faster at 4000 rows and near-linear scaling vs the original. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the if/else duplication in clear(): empty path_to_row once at the top, then clear the store and emit only when it has rows. Same behavior, less duplication. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What
Three focused commits on
blueman/gui/DeviceList.py, each building on the previous:discover_devicesstarted aGLib.timeoutforupdate_progressbut discarded the source id, sostop_discovery()left it running until the callback next observeddiscovering=False. Store the id, remove it instop_discovery()via_remove_discovery_timeout, and null it on the callback's own exit paths so a self-ending timer is never double-removed.clear()looped over the liststore callingdevice_remove_eventper row. EachGenericList.delete()calls the O(n)Gtk.ListStore.iter_is_valid, making the whole clear O(n²), and it mutated the liststore while iterating it before callingliststore.clear()anyway. Replaced with a singleliststore.clear()— fixes the iterate-while-mutating bug and drops the redundant pass.path_to_rowstill held a liveGtk.TreeRowReferenceper row forced GTK to update every reference as rows were removed (still ~O(n²)). Resetpath_to_rowbeforeclear()so there are no live references to keep in sync.Why
On adapter switch,
populate_devices, and adapter removal,clear()runs over the full device list. With many paired/seen devices it was quadratic and relied on a buggy mutate-while-iterating loop.Proof
test/benchmarks/bench_devicelist_clear.pyexercises the original, step-2, and step-3 strategies on a realGtk.ListStorewith a matchingTreeRowReferencecache:The decisive win is releasing references before clearing (~4.3× vs original at 4000 rows, near-linear vs quadratic). Note that a single
clear()with the references still alive is not faster on its own — GTK keeps every live reference in sync per removal — which is why step 3 is a distinct change rather than a duplicate of step 2.Tests
test/gui/test_devicelist.py(13 cases): discovery-timer store / cancel / null paths, the no-double-remove completion case, andclear()on empty/non-empty stores, reference release, and a size fuzz (0…1000). ruff + flake8 clean.Note on behaviour
clear()keeps the same observable result (empty store, empty cache, onedevice-selected(None, None)emit when non-empty); the per-rowdevice-selectedemits the old loop produced for the selected row were redundant with the final emit.