Skip to content

perf(gui): clear DeviceList in one pass and fix discovery timer lifecycle - #3316

Open
geraldo-netto wants to merge 4 commits into
blueman-project:mainfrom
geraldo-netto:perf/devicelist-clear-discovery
Open

perf(gui): clear DeviceList in one pass and fix discovery timer lifecycle#3316
geraldo-netto wants to merge 4 commits into
blueman-project:mainfrom
geraldo-netto:perf/devicelist-clear-discovery

Conversation

@geraldo-netto

Copy link
Copy Markdown
Contributor

What

Three focused commits on blueman/gui/DeviceList.py, each building on the previous:

  1. fix: track and cancel the discovery progress timerdiscover_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.
  2. perf: clear in one pass instead of per-row deletionclear() looped over the liststore calling device_remove_event per row. Each GenericList.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 calling liststore.clear() anyway. Replaced with a single liststore.clear() — fixes the iterate-while-mutating bug and drops the redundant pass.
  3. perf: release row references before clearing the store — clearing while path_to_row still held a live Gtk.TreeRowReference per row forced GTK to update every reference as rows were removed (still ~O(n²)). Reset path_to_row before clear() 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.py exercises the original, step-2, and step-3 strategies on a real Gtk.ListStore with a matching TreeRowReference cache:

rows original one-pass clear (live refs) release-then-clear
4000 162 ms 231 ms 37 ms

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, and clear() 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, one device-selected(None, None) emit when non-empty); the per-row device-selected emits the old loop produced for the selected row were redundant with the final emit.

geraldo-netto and others added 4 commits June 19, 2026 13:08
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>
@sonarqubecloud

Copy link
Copy Markdown

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