perf(bluez): serve Base property reads from the proxy cache#3315
Open
geraldo-netto wants to merge 4 commits into
Open
perf(bluez): serve Base property reads from the proxy cache#3315geraldo-netto wants to merge 4 commits into
geraldo-netto wants to merge 4 commits into
Conversation
Object-identity caching lived inline in BaseMeta and, via hasattr, leaked across subclasses. Move it into a named InstanceRegistry held per concrete class (cls.__dict__), and deregister an instance on destroy(). Pure structural change; behaviour preserved. Adds test/bluez/test_base.py with a FakeProxy harness reused by later commits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Base.get issued a synchronous Properties.Get on every read, on the GLib main loop. The Gio proxy already caches properties and keeps them current from PropertiesChanged, so read get_cached_property first and fall back to a synchronous Get only on a cache miss. Benchmark test/benchmarks/bench_bluez_base_get.py models each call_sync as one D-Bus round-trip: 35000 repeated reads drop from 35000 to 0 when the cache is warm. Tests cover cached hit, miss->sync fallback, PropertiesChanged refresh, and the __fallback default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get(name, fresh=True) forces a synchronous refresh; on a failed refresh the last cached value is still served but the property is flagged stale; PropertiesChanged or a later successful refresh clears the flag. is_stale(name) lets call sites that need live state check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A failed refresh silently fell back to the cached property. Emit a logging.debug naming the interface/property and the error so the stale-serve is diagnosable. Rounds out test_base.py to ~99% coverage of Base.py (set/get_properties/__contains__/_call paths), an explicit disconnect-via- PropertiesChanged regression, plus adversarial fuzz over get() names and PropertiesChanged payloads. 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
Rework
blueman/bluez/Base.pyso device/adapter property reads are served from the Gio proxy's local cache instead of a synchronous D-Bus round-trip on every access, and make the cache's freshness explicit.Four focused commits, each building on the previous:
InstanceRegistry— object-identity caching lived inline inBaseMetaand, viahasattr, leaked across subclasses. Moved to a named per-class registry (cls.__dict__); instances deregister ondestroy(). Pure structural change.get()—get()issued a synchronousProperties.Geton the GLib main loop for every read. The proxy already caches properties and keeps them current viaPropertiesChanged, so readget_cached_propertyfirst and fall back to a synchronousGetonly on a cache miss.get(name, fresh=True)forces a refresh; a failed refresh still serves the cached value but flags it stale;PropertiesChangedor a later successful refresh clears the flag.is_stale(name)lets callers that need live state check.logging.debug(interface, property, error) when a cached value is served after a bus error, so stale-serves are diagnosable.Why
device["Prop"]is read repeatedly by the manager UI; each read was a blocking system-bus round-trip on the GLib main loop.Proof
test/benchmarks/bench_bluez_base_get.pymodels eachcall_syncas one D-Bus round-trip. 35000 repeated reads of warm properties:Correctness / disconnect handling
Reads now return the proxy's signal-tracked cached value instead of forcing a fetch each call. This does not break disconnect handling (manual or link loss):
PropertiesChanged{Connected=false}.Gio.DBusProxy(flagsNONE) updates its cached property first, then emitsg-properties-changed. blueman's_properties_changedfires on that same signal and drives the row update. The cache update and the UI update are the same event, in order — any read in response sees the fresh value.Getthat errored on the dead object and fell back to the cache anyway, so behaviour is equivalent — just without the error log. blueman destroys theDeviceonInterfacesRemovedregardless._calland the UI reacts to the signal.get(name, fresh=True)is the escape hatch if such a path is ever added.Audited every synchronous
device["Connected"]read (ManagerDeviceListsetup/timer/update,NetworkService.available,DisconnectItems,PulseAudioProfile,ShowConnected): all are setup-time, signal-driven, or menu-build reads of last-known state — none depend on a forced live fetch.Tests
test/bluez/test_base.py(28 cases, ~99% coverage ofBase.py) with aFakeProxyharness: registry/identity, cache hit vs miss,PropertiesChangedrefresh, an explicit disconnect-via-PropertiesChangedregression (assertsConnectedflips toFalsewith zero sync calls), freshness/stale transitions, the cached-fallback log, async_callreply/error/unhandled paths, plus adversarial fuzz overget()names andPropertiesChangedpayloads. ruff + flake8 clean.