Skip to content

perf: manager device list - #3306

Open
geraldo-netto wants to merge 9 commits into
blueman-project:mainfrom
geraldo-netto:fix/manager-device-list-perf
Open

perf: manager device list#3306
geraldo-netto wants to merge 9 commits into
blueman-project:mainfrom
geraldo-netto:fix/manager-device-list-perf

Conversation

@geraldo-netto

@geraldo-netto geraldo-netto commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Batch manager device row setup/update property reads using get_properties() snapshots.
  • Replace per-device power-level timers with one shared timer over monitored devices, driven by the cached connected state instead of a live D-Bus read per device per second.
  • Cache device UUIDs, class, address and icon name in manager row data and use the cached state for drag motion, drag-and-drop send, double-click connect handling, Ctrl+C address copy, the visibility filter, tooltip reads and menu generation.
  • Share a single GetAll snapshot when adding device rows (DeviceList.add_device, DeviceSelectorList) instead of three to four round trips per discovered device.
  • Batch GenericList.set column writes into a single row-changed emission, so the sort machinery and visibility filter run once per update instead of once per column.
  • Cache signal bar pixbufs per (bar, level, scale) instead of decoding the PNG on every level change, and skip the D-Bus battery read used only for a debug log line when debug logging is disabled.
  • Add benchmark script, expand unit test coverage and add deterministic fuzz-style tests.

Measured (10000 iterations, DBUS_RTT=200us, /usr/bin/python3):

Metric main (fb41890) branch (24cdca0) Gain
D-Bus round-trips/iter 57 1 98.2% fewer
Round-trips total 570000 10000 98.2% fewer
cpu_seconds 1.37 1.35 1.8% faster
Modeled wall time 115.37s 3.35s 97.1% faster
Speedup factor 1.0x 34.4x 34.4x

Benchmarks the row_setup_event/row_update_event hot path by modeling each device property access as one synchronous D-Bus round-trip. run_compare.sh diffs the current branch against a base ref (default main) and asserts a minimum gain.

Why the gain: every device["Key"] resolves to one synchronous org.freedesktop.DBus.Properties.Get via Gio call_sync (blueman/bluez/Base.py get()). main reads each property individually and re-runs row_update_event four times inside row_setup_event, so a single row costs ~57 round-trips. The branch calls get_properties() once (one Properties.GetAll) per row lifecycle, caches uuids/class/address/icon in the liststore and keeps them fresh from property-changed signals, so setup costs exactly one round trip and property updates cost zero. D-Bus round-trips dominate wall time, so collapsing them is the gain; pure-Python CPU is near-flat between versions.

Tests

  • /usr/bin/python3 -m unittest test.gui.manager.test_manager_device_list test.gui.test_device_lists (36 tests)
  • Deterministic fuzz-style tests: a 2000-event randomized property state machine over row_update_event asserting the cached row state always matches a reference model and the device proxy is never read, plus randomized sweeps over _has_objpush and make_display_name.
  • Changed-line coverage vs main (stdlib trace): ManagerDeviceList 94%, GenericList / DeviceList / DeviceSelectorList 100%. ManagerDeviceMenu.generate remains untested (needs a real Gtk.Menu and installed GSettings schemas), as before this PR.
  • test/benchmarks/run_compare.sh 10000 main passes (numbers above).

@geraldo-netto geraldo-netto changed the title Fix/manager device list perf perf: manager device list Jun 15, 2026
@infirit

infirit commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

How much of a performance impact are you seeing? It may be negative as getting all the properties instead of a couple will result in more dbus calls in the end.

The best performance increase will be using the cached properties on the proxy. However last time I tried I ran into issues with the singleton design and most likely stale proxies.

I haven't looked too closely but you don't need to cast NewType, just call the newtypes like BTAddress on the value and type checkers will pick the correct type.

@geraldo-netto

Copy link
Copy Markdown
Contributor Author

Hi @infirit , let me add some proper benchmarks, as I mentioned to @cschramm , I was vibe coding these things and some of these come as suggestions from claude

@geraldo-netto

geraldo-netto commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

@infirit @cschramm
I'm adding the benchmarks now and the microbenchmark says:
but let me finish the microbenchmark, so, I can add it too to avoid performance regressions

image

Benchmarks the row_setup_event/row_update_event hot path by modeling each
device property access as one synchronous D-Bus round-trip. run_compare.sh
diffs the current branch against a base ref (default main) and asserts a
minimum gain.

Why the gain: every device["Key"] resolves to one synchronous
org.freedesktop.DBus.Properties.Get via Gio call_sync (blueman/bluez/Base.py
get()). main reads each property individually and re-runs row_update_event
four times inside row_setup_event, so a single row costs ~57 round-trips.
The branch calls get_properties() once (one Properties.GetAll), caches uuids
in the liststore, and batches the set() calls, cutting the path to ~5
round-trips. D-Bus round-trips dominate wall time, so collapsing them is the
gain; pure-Python CPU is near-flat between versions.

Measured (10000 iterations, DBUS_RTT=200us, /usr/bin/python3):

  metric                  main (fb41890)  branch (af0d8fe)  gain
  ----------------------  ---------------  -----------------  ---------
  D-Bus round-trips/iter  57               5                  91.2% fewer
  round-trips total       570000           50000              91.2% fewer
  cpu_seconds             4.54             4.15               8.5% faster
  modeled wall time       118.54s          14.15s             88.1% faster
  speedup factor          1.0x             8.4x               8.4x

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@geraldo-netto
geraldo-netto force-pushed the fix/manager-device-list-perf branch from e1e13e8 to c100da1 Compare June 19, 2026 09:01
@geraldo-netto

Copy link
Copy Markdown
Contributor Author

@infirit , could you please review it now?
I also have added to the PR body the numbers and why it works and fixed the flake8 issue. I think we are good to go

@infirit

infirit commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Apologies I should have looked closer. I forgot that GetProperties is a single dbus call 😓. I'll do some testing over the weekend.

geraldo-netto and others added 5 commits June 19, 2026 11:31
Per review: NewType values can be constructed directly (BtAddress(value));
type checkers infer the correct type without typing.cast.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GenericList.set wrote columns one liststore.set call at a time, emitting
one row-changed per column. Every emission re-runs the sort machinery
and the visibility filter, so a multi-column row update paid that cost
repeatedly. Passing all columns in a single call emits row-changed once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every device["Key"] is a synchronous Properties.Get round trip and
get_properties() is one GetAll. Cache the remaining hot properties in
row state and share one snapshot across the add path:

- Cache Class, Address and Icon in liststore columns (kept fresh from
  property-changed signals, mirroring the GetAll fallbacks for
  invalidated properties). Icon rebuilds and Alias updates in
  row_update_event now run without any D-Bus calls, as do filter_func
  (evaluated on every row-changed), drag-and-drop send, Ctrl+C address
  copy, double-click connect and menu generation.
- The per-second power-level timer read Connected live from D-Bus for
  every monitored device; use the cached column instead.
- add_device did three round trips per discovered device (Adapter get,
  GetAll for the Name check, GetAll in row_setup_event); do one GetAll
  and pass the snapshot through row_setup_event. Same for
  DeviceSelectorList, which did four separate property reads per row.
- Cache signal bar pixbufs per (bar, level, scale) instead of decoding
  the PNG on every level change, and stop reading the battery
  percentage over D-Bus just to build a debug log line when debug
  logging is disabled.

Benchmark (test/benchmarks): 5 -> 1 modeled round trips per row
lifecycle vs the previous commit, 57 -> 1 vs main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend the headless unit tests to cover the paths the perf work
touches: GenericList.set batching (row-changed emission count against a
real ListStore), single-GetAll add_device including the foreign-adapter
and named-device branches, and DeviceSelectorList row setup.

Add deterministic fuzz-style tests: a 2000-event randomized property
state machine over row_update_event asserting the cached row state
always matches a reference model and the device proxy is never read,
plus randomized sweeps over _has_objpush and make_display_name.

Changed-line coverage vs main: ManagerDeviceList 94%, GenericList,
DeviceList and DeviceSelectorList 100% (ManagerDeviceMenu.generate
still needs a real Gtk.Menu and GSettings schemas, so it stays
untested, as before).

Also list the test modules in EXTRA_DIST so release tarballs include
them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@geraldo-netto
geraldo-netto force-pushed the fix/manager-device-list-perf branch from 4a08679 to 78cfefc Compare July 8, 2026 19:37
@sonarqubecloud

sonarqubecloud Bot commented Jul 8, 2026

Copy link
Copy Markdown

@geraldo-netto

Copy link
Copy Markdown
Contributor Author

@infirit @cschramm I managed to find some more optimization opportunities, I have updated the PR

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.

2 participants