Skip to content

Timeline calendar entity pegs the event loop on every event (full DB reload + purge in async_update) — 1.7.0 #673

Description

@moshemalawach

Describe the bug

After updating to 1.7.0, the calendar.llm_vision_timeline entity pins one CPU core at ~100% continuously and starves the Home Assistant asyncio event loop. HA logs, on repeat:

WARNING [homeassistant.helpers.entity] Update of calendar.llm_vision_timeline is taking over 10 seconds
WARNING [homeassistant.util.loop] Detected blocking call to open with args ('/usr/share/zoneinfo/Europe/Budapest', 'rb') inside the event loop
WARNING [custom_components.llmvision.timeline] [CLEANUP] Failed to remove /media/llmvision/snapshots/<uuid>-cam...

Because the event loop is saturated, unrelated integrations start timing out (in my case custom_components.frigate.api: Timeout error fetching ... — Frigate itself responds in <15 ms, the timeout is purely loop starvation).

I confirmed the culprit thread by sampling /proc/<pid>/task/*/stat from the host: the MainThread (event loop) is the consumer, not the recorder or executor. Disabling the calendar.llm_vision_timeline entity immediately drops the core to idle (MainThread 66% → 10%, load 1.2 → 0.4) and the Frigate timeouts stop. Re-enabling brings the pegged core back.

This is not a data-volume problem: my events.db is 1795 rows / 1.2 MB (~1 week of events). A reload over that should be milliseconds.

Root cause (code analysis, 1.7.0)

In calendar.py:

  • Calendar.async_added_to_hass() connects SIGNAL_TIMELINE_UPDATED to async_schedule_update_ha_state(force_refresh=True) — so every timeline write (i.e. every camera event) forces a full async_update.
  • Calendar.async_update() calls timeline.load_events() then get_all_events() and rebuilds all CalendarEvent objects from scratch each time.
  • Calendar.async_get_events() (called when the calendar card is viewed) also calls load_events() — a full reload per request.

In timeline.py:

  • load_events() (~L595) runs await self._purge_expired_events() and SELECT uid,title,start,end,... FROM events with no filter, then parses every row (dt_util.parse_datetime + _ensure_datetime) on every call.
  • _purge_expired_events() runs on every load_events() and appears to do blocking filesystem work on snapshots (hence the repeated [CLEANUP] Failed to remove ... warnings) plus per-event timezone conversions that trigger blocking zoneinfo file opens on the loop.

Net effect: O(n) DB reload + purge + blocking file/zoneinfo I/O on the event loop, executed on every event write and amplified by event frequency. Even at ~1.8k rows it exceeds the 10 s warning threshold and never lets the loop breathe, degrading the entire HA instance.

Suggested fixes

  • Don't full-reload on every SIGNAL_TIMELINE_UPDATED; debounce, or update the in-memory list incrementally / only recompute the "current event".
  • Decouple _purge_expired_events() from load_events() (run it on a timer, not on every read), and move all blocking file/os.remove work to async_add_executor_job.
  • In async_get_events(), query the DB with a WHERE start/end range filter instead of loading the whole table.
  • Resolve the timezone once instead of triggering per-event zoneinfo opens on the loop.

Workaround

Disable the calendar.llm_vision_timeline entity (Settings → Devices & Services → Entities). llmvision analysis/notifications keep working; only the calendar timeline entity is lost.

Version of the integration

1.7.0

Version of Home Assistant

2026.6.3

Additional context

Install: HA in Docker (Alpine image), SQLite recorder, ~2500 entities. Timeline retention default. Happy to test a patch / provide more logs or a profile.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions