Skip to content

perf(entity): scope availability subscription to the lock entities#1306

Open
terafin wants to merge 1 commit into
raman325:mainfrom
intarweb:perf/availability-subscription-scope
Open

perf(entity): scope availability subscription to the lock entities#1306
terafin wants to merge 1 commit into
raman325:mainfrom
intarweb:perf/availability-subscription-scope

Conversation

@terafin

@terafin terafin commented Jun 27, 2026

Copy link
Copy Markdown

Proposed change

The availability binary_sensor registers its state-change handler with all_states=True:

async_track_state_change_filtered(
    self.hass, TrackStates(True, set(), set()), self._handle_available_state_update
)

TrackStates(True, ...) subscribes to every state change in the entire Home Assistant instance. _handle_available_state_update then filters in Python and early-returns for any entity that isn't one of the slot's locks.

On a production instance with ~6–7 of these entities, cProfile measured the handler running ~86,580 times / 30s (~2,900/sec) and its inner generator ~640,100 times / 30s — the single largest application-level CPU consumer, doing no useful work.

Fix

Scope the subscription to just the lock entities:

TrackStates(False, {lock.lock.entity_id for lock in self.locks}, set())

self.locks is mutated at runtime (_handle_add_locks / _handle_remove_lock), so the tracked set is re-scoped via async_update_listeners() whenever locks change, and availability is recomputed at that point — previously the all-states firehose recomputed it implicitly, so this preserves behavior for the dynamic add/remove-lock case. The initial availability computation and the async_on_remove cleanup are unchanged.

Pure performance fix; no behavior change. Verified: the full test_binary_sensor.py, test_event.py, and test_callbacks.py suites pass (48 tests), and a new regression test asserts the subscription is scoped to the locks and re-scopes on lock add/remove.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (which adds functionality)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • Performance regression measured via cProfile on a production HA instance running 4.2.0.
  • No existing issue; filing this PR directly.

🤖 Generated with Claude Code

The availability binary_sensor registered its state-change handler with
`async_track_state_change_filtered(hass, TrackStates(True, set(), set()), ...)`.
`all_states=True` subscribes to EVERY state change in the Home Assistant
instance; the handler then filters in Python and early-returns for any entity
that isn't one of the slot's locks.

On a production instance with ~6-7 of these entities, cProfile measured the
handler running ~86,580 times / 30s (~2,900/sec) and its inner generator
~640,100 times / 30s — the single largest application-level CPU consumer,
doing no useful work.

Scope the subscription to just the lock entities via
`TrackStates(False, {lock.lock.entity_id for lock in self.locks}, set())`.
Because `self.locks` is mutated at runtime by `_handle_add_locks` /
`_handle_remove_lock`, the tracked set is re-scoped with
`async_update_listeners` whenever locks change, and availability is recomputed
at that point (previously the all-states firehose recomputed it implicitly).

Pure performance fix; no behavior change. Adds a regression test asserting the
subscription is scoped to the locks and re-scopes on lock add/remove.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added python Pull requests that update Python code bug Something isn't working labels Jun 27, 2026
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 96.94%. Comparing base (dc1ed4f) to head (b390e7d).
⚠️ Report is 13 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
custom_components/lock_code_manager/entity.py 91.66% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1306      +/-   ##
==========================================
- Coverage   96.96%   96.94%   -0.03%     
==========================================
  Files          53       53              
  Lines        6364     6374      +10     
  Branches      473      473              
==========================================
+ Hits         6171     6179       +8     
- Misses        193      195       +2     
Flag Coverage Δ
python 97.47% <91.66%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
custom_components/lock_code_manager/entity.py 94.89% <91.66%> (-1.18%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@raman325 raman325 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the contribution! Only one question/comment

because the added/removed lock's own state change will not otherwise
trigger an update.
"""
if (tracker := self._available_state_tracker) is None:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this guard is necessary, I believe by this point the tracker will be set

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants