Skip to content

Run the sensors' skyfield searches in the executor - #6

Open
jrx-code wants to merge 4 commits into
okkine:mainfrom
jrx-code:perf/offload-skyfield-to-executor
Open

jrx-code wants to merge 4 commits into
okkine:mainfrom
jrx-code:perf/offload-skyfield-to-executor

Conversation

@jrx-code

Copy link
Copy Markdown

Closes #2.

RiseSensor, SetSensor, TransitSensor and AntitransitSensor do all their ephemeris work inline in async_update, and async_added_to_hass awaits it, so every entity's first update runs its searches on the event loop. With ten bodies that is enough to trip Home Assistant's own warning:

Setup of sensor platform sol is taking over 10 seconds.

and to stall everything else in the instance while it happens.

The change

Each of those four classes gets a plain _compute() holding exactly the work it did before; async_update hands that to async_add_executor_job and only writes the state afterwards. AzimuthSensor and ElevationSensor keep their structure, because they await the ephemeris cache mid-update, but their find_step_aligned_targets calls, the expensive part, go through the executor too.

No search was added, removed or reordered. I verified that mechanically: for all ten sensor classes, the set and the source order of observer calls (with the executor and partial wrappers unwrapped) is identical before and after.

Measurements

Bare Home Assistant 2026.2.3, one location, all ten bodies, every sensor enabled (64 entities). A 20 ms heartbeat runs alongside the platform setup; how late it fires is how long the loop was blocked. Two cold runs each (storage wiped between runs):

loop blocked, total longest single stall stalls over 500 ms
before 23.3 s / 14.7 s 2681 ms / 1577 ms 11 / 10
after 5.8 s / 2.7 s 795 ms / 222 ms 3 / 0

Setup wall time is unchanged (25.3 / 16.5 s before, 22.9 / 14.8 s after): the same work happens, just not on the loop.

All 64 entities come out with the same states and attributes before and after, allowing 0.5 deg and 120 s of drift since the two runs are minutes apart. Zero differences.

🤖 Generated with Claude Code

jrx-code and others added 4 commits October 26, 2026 19:36
Adding a timedelta to a timezone-aware datetime is wall-clock arithmetic
and resets `fold`. Inside the autumn DST fold `candidate + timedelta(
seconds=1)` can therefore land a full hour BEFORE `candidate` in absolute
time, so the `while search_pos < ...` loops in TransitSensor and
AntitransitSensor keep re-finding the same event and never terminate.

Because the search runs synchronously in `async_added_to_hass`, the whole
Home Assistant event loop is blocked: the instance accepts TCP connections
on 8123 but never answers them.

Reproduced with Europe/Warsaw on 2026-10-25, when the Uranus transit fell
at 02:00:24 fold=1:

    candidate      -> UTC 2026-10-25 01:00:24
    candidate + 1s -> UTC 2026-10-25 00:00:25   (one hour earlier)

Since `_RISE_SET_PRIOR_WINDOW_DAYS` is 14, every restart in the two weeks
after a DST change hits this.

Fix: `_advance_one_second()` performs the step in UTC, keeping it
monotonic, and three `if candidate <= last_found: break` guards make the
loops terminate even if another non-monotonic step is ever introduced.

Verified offline against real ephemerides with ZoneInfo('Europe/Warsaw'):
uranus went from >300 iterations without terminating to 4 iterations in
0.1 s; mercury, saturn and neptune returned identical results before and
after.

Refs: okkine#3, okkine#2

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Keeps the fork installable from HACS as a custom repository until the
DST fix lands upstream (okkine#4).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rise, Set, Transit and Antitransit did all their ephemeris work inline in
async_update, which async_added_to_hass awaits, so every entity's first update
ran its searches on the event loop. With ten bodies that is enough to trip
Home Assistant's own warning, "Setup of sensor platform sol is taking over 10
seconds" (issue okkine#2), and to stall everything else running in the instance.

Each of those four classes now has a plain _compute() holding exactly the work
it did before; async_update hands that to async_add_executor_job and only
writes the state. Azimuth and Elevation keep their structure (they await the
ephemeris cache mid-update) but their find_step_aligned_targets calls, the
expensive part, now go through the executor as well.

Measured on a bare Home Assistant 2026.2.3 with one location, all ten bodies
and every sensor enabled (64 entities), timing how late a 20 ms heartbeat
fires while the platform sets up, two cold runs each:

                        event loop blocked    longest stall    stalls > 500 ms
  before                23.3 s / 14.7 s       2681 / 1577 ms   11 / 10
  after                  5.8 s /  2.7 s        795 /  222 ms    3 /  0

Setup wall time is unchanged, as expected: the same work is done, just off the
loop. All 64 entities come out with the same states and attributes before and
after (compared allowing 0.5 deg and 120 s of drift between runs, since the
runs are minutes apart).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

Setup of sensor platform sol is taking over 10 seconds.

1 participant