Avoid reschedule churn in Storage.async_delay_save (#111091)

* Avoid circular import in Storage.async_delay_save

We call Storage.async_delay_save for every entity being added or removed
from the registry. The late import took more time than everything else
in the function.

* Avoid reschedule churn in Storage.async_delay_save

When we are adding or removing entities we will call async_delay_save
quite often which has to add and remove a TimerHandle on the event loop
which can add up when there are a lot of registry items changing.

If the timer handle still has 80% of the time remaining on it
we will avoid resceduling and let it fire at the time the
original async_delay_save call was made. This ensures we
do not force the event loop to rebuild its heapq because
too many timer handlers were cancelled at once

* div0

* add coverage for 0 since we had none

* fix bad conflict

* tweaks

* tweaks

* tweaks

* tweaks

* tweaks

* tweaks

* more test fixes

* mqtt tests rely on event loop overhead
This commit is contained in:
J. Nick Koston 2024-02-23 21:46:00 -10:00 committed by GitHub
parent ff0e0b3e77
commit 5b8591ec7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 149 additions and 18 deletions

View file

@ -6,6 +6,7 @@ import os
from typing import Any, NamedTuple
from unittest.mock import Mock, patch
from freezegun.api import FrozenDateTimeFactory
import py
import pytest
@ -19,7 +20,11 @@ from homeassistant.helpers import issue_registry as ir, storage
from homeassistant.util import dt as dt_util
from homeassistant.util.color import RGBColor
from tests.common import async_fire_time_changed, async_test_home_assistant
from tests.common import (
async_fire_time_changed,
async_fire_time_changed_exact,
async_test_home_assistant,
)
MOCK_VERSION = 1
MOCK_VERSION_2 = 2
@ -115,7 +120,7 @@ async def test_loading_parallel(
async def test_saving_with_delay(
hass: HomeAssistant, store, hass_storage: dict[str, Any]
hass: HomeAssistant, store: storage.Store, hass_storage: dict[str, Any]
) -> None:
"""Test saving data after a delay."""
store.async_delay_save(lambda: MOCK_DATA, 1)
@ -131,6 +136,88 @@ async def test_saving_with_delay(
}
async def test_saving_with_delay_churn_reduction(
hass: HomeAssistant,
store: storage.Store,
hass_storage: dict[str, Any],
freezer: FrozenDateTimeFactory,
) -> None:
"""Test saving data after a delay with timer churn reduction."""
store.async_delay_save(lambda: MOCK_DATA, 1)
assert store.key not in hass_storage
freezer.tick(0.2)
async_fire_time_changed_exact(hass)
await hass.async_block_till_done()
assert store.key not in hass_storage
freezer.tick(1)
async_fire_time_changed_exact(hass)
await hass.async_block_till_done()
assert hass_storage[store.key] == {
"version": MOCK_VERSION,
"minor_version": 1,
"key": MOCK_KEY,
"data": MOCK_DATA,
}
del hass_storage[store.key]
# Simulate what some of the registries do when they add 100 entities
for _ in range(100):
store.async_delay_save(lambda: MOCK_DATA, 1)
freezer.tick(0.2)
async_fire_time_changed_exact(hass)
await hass.async_block_till_done()
assert store.key not in hass_storage
store.async_delay_save(lambda: MOCK_DATA, 1)
freezer.tick(1)
async_fire_time_changed_exact(hass)
await hass.async_block_till_done()
assert store.key in hass_storage
del hass_storage[store.key]
store.async_delay_save(lambda: MOCK_DATA, 1)
freezer.tick(0.5)
async_fire_time_changed_exact(hass)
await hass.async_block_till_done()
assert store.key not in hass_storage
store.async_delay_save(lambda: MOCK_DATA, 1)
freezer.tick(0.8)
async_fire_time_changed_exact(hass)
await hass.async_block_till_done()
assert store.key not in hass_storage
store.async_delay_save(lambda: MOCK_DATA, 1)
freezer.tick(0.8)
async_fire_time_changed_exact(hass)
await hass.async_block_till_done()
assert store.key not in hass_storage
freezer.tick(0.2)
async_fire_time_changed_exact(hass)
await hass.async_block_till_done()
assert store.key in hass_storage
# Make sure if we do another delayed save
# and one with a shorter delay, the shorter delay wins
del hass_storage[store.key]
store.async_delay_save(lambda: MOCK_DATA, 2)
freezer.tick(0.2)
async_fire_time_changed_exact(hass)
await hass.async_block_till_done()
assert store.key not in hass_storage
store.async_delay_save(lambda: MOCK_DATA, 1)
freezer.tick(1.0)
async_fire_time_changed_exact(hass)
await hass.async_block_till_done()
assert store.key in hass_storage
async def test_saving_on_final_write(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
@ -281,6 +368,23 @@ async def test_multiple_delay_save_calls(
assert data == {"delay": "no"}
async def test_delay_save_zero(
hass: HomeAssistant, store: storage.Store, hass_storage: dict[str, Any]
) -> None:
"""Test async_delay_save accepts 0."""
store.async_delay_save(lambda: {"delay": "0"}, 0)
# sleep is to run one event loop to get the task scheduled
await asyncio.sleep(0)
await hass.async_block_till_done()
assert store.key in hass_storage
assert hass_storage[store.key] == {
"version": MOCK_VERSION,
"minor_version": 1,
"key": MOCK_KEY,
"data": {"delay": "0"},
}
async def test_multiple_save_calls(
hass: HomeAssistant, store, hass_storage: dict[str, Any]
) -> None:
@ -706,7 +810,7 @@ async def test_os_error_is_fatal(tmpdir: py.path.local) -> None:
async def test_read_only_store(
hass: HomeAssistant, read_only_store, hass_storage: dict[str, Any]
hass: HomeAssistant, read_only_store: storage.Store, hass_storage: dict[str, Any]
) -> None:
"""Test store opened in read only mode does not save."""
read_only_store.async_delay_save(lambda: MOCK_DATA, 1)