Group loading of translations for integrations to reduce executor jobs at startup (#110674)

This commit is contained in:
J. Nick Koston 2024-02-17 21:08:55 -06:00 committed by GitHub
parent def6c5c21c
commit 16653ff5d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 73 additions and 98 deletions

View File

@ -710,6 +710,21 @@ async def _async_resolve_domains_to_setup(
requirements.async_load_installed_versions(hass, needed_requirements), requirements.async_load_installed_versions(hass, needed_requirements),
"check installed requirements", "check installed requirements",
) )
# Start loading translations for all integrations we are going to set up
# in the background so they are ready when we need them. This avoids a
# lot of waiting for the translation load lock and a thundering herd of
# tasks trying to load the same translations at the same time as each
# integration is loaded.
#
# We do not wait for this since as soon as the task runs it will
# hold the translation load lock and if anything is fast enough to
# wait for the translation load lock, loading will be done by the
# time it gets to it.
hass.async_create_background_task(
translation.async_load_integrations(hass, {*BASE_PLATFORMS, *domains_to_setup}),
"load translations",
)
return domains_to_setup, integration_cache return domains_to_setup, integration_cache

View File

@ -158,7 +158,7 @@ async def _async_get_component_strings(
"""Load translations.""" """Load translations."""
translations: dict[str, Any] = {} translations: dict[str, Any] = {}
# Determine paths of missing components/platforms # Determine paths of missing components/platforms
files_to_load = {} files_to_load: dict[str, str] = {}
for loaded in components: for loaded in components:
domain = loaded.partition(".")[0] domain = loaded.partition(".")[0]
if not (integration := integrations.get(domain)): if not (integration := integrations.get(domain)):
@ -264,13 +264,13 @@ class _TranslationCache:
_LOGGER.debug( _LOGGER.debug(
"Cache miss for %s: %s", "Cache miss for %s: %s",
language, language,
", ".join(components), components,
) )
# Fetch the English resources, as a fallback for missing keys # Fetch the English resources, as a fallback for missing keys
languages = [LOCALE_EN] if language == LOCALE_EN else [LOCALE_EN, language] languages = [LOCALE_EN] if language == LOCALE_EN else [LOCALE_EN, language]
integrations: dict[str, Integration] = {} integrations: dict[str, Integration] = {}
domains = list({loaded.partition(".")[0] for loaded in components}) domains = {loaded.partition(".")[0] for loaded in components}
ints_or_excs = await async_get_integrations(self.hass, domains) ints_or_excs = await async_get_integrations(self.hass, domains)
for domain, int_or_exc in ints_or_excs.items(): for domain, int_or_exc in ints_or_excs.items():
if isinstance(int_or_exc, Exception): if isinstance(int_or_exc, Exception):
@ -392,31 +392,15 @@ async def async_get_translations(
""" """
if integrations is None and config_flow: if integrations is None and config_flow:
components = (await async_get_config_flows(hass)) - hass.config.components components = (await async_get_config_flows(hass)) - hass.config.components
elif integrations is not None:
components = set(integrations)
else: else:
components = _async_get_components(hass, category, integrations) components = _async_get_components(hass, category)
cache: _TranslationCache = hass.data[TRANSLATION_FLATTEN_CACHE] return await _async_get_translations_cache(hass).async_fetch(
language, category, components
return await cache.async_fetch(language, category, components)
async def _async_load_translations(
hass: HomeAssistant,
language: str,
category: str,
integration: str | None,
) -> None:
"""Prime backend translation cache.
If integration is not specified, translation cache is primed for all loaded integrations.
"""
components = _async_get_components(
hass, category, [integration] if integration is not None else None
) )
cache = hass.data[TRANSLATION_FLATTEN_CACHE]
await cache.async_load(language, components)
@callback @callback
def async_get_cached_translations( def async_get_cached_translations(
@ -430,42 +414,36 @@ def async_get_cached_translations(
If integration is specified, return translations for it. If integration is specified, return translations for it.
Otherwise, default to all loaded integrations. Otherwise, default to all loaded integrations.
""" """
components = _async_get_components( if integration is not None:
hass, category, [integration] if integration is not None else None components = {integration}
else:
components = _async_get_components(hass, category)
return _async_get_translations_cache(hass).get_cached(
language, category, components
) )
@callback
def _async_get_translations_cache(hass: HomeAssistant) -> _TranslationCache:
"""Return the translation cache."""
cache: _TranslationCache = hass.data[TRANSLATION_FLATTEN_CACHE] cache: _TranslationCache = hass.data[TRANSLATION_FLATTEN_CACHE]
return cache.get_cached(language, category, components) return cache
_DIRECT_MAPPED_CATEGORIES = {"state", "entity_component", "services"}
@callback @callback
def _async_get_components( def _async_get_components(
hass: HomeAssistant, hass: HomeAssistant,
category: str, category: str,
integrations: Iterable[str] | None = None,
) -> set[str]: ) -> set[str]:
"""Return a set of components for which translations should be loaded.""" """Return a set of components for which translations should be loaded."""
if integrations is not None: if category in _DIRECT_MAPPED_CATEGORIES:
components = set(integrations) return hass.config.components
elif category in ("state", "entity_component", "services"): # Only 'state' supports merging, so remove platforms from selection
components = hass.config.components return {component for component in hass.config.components if "." not in component}
else:
# Only 'state' supports merging, so remove platforms from selection
components = {
component for component in hass.config.components if "." not in component
}
return components
async def _async_load_state_translations_to_cache(
hass: HomeAssistant,
language: str,
integration: str | None,
) -> None:
"""Load state translations to cache."""
await _async_load_translations(hass, language, "entity", integration)
await _async_load_translations(hass, language, "state", integration)
await _async_load_translations(hass, language, "entity_component", integration)
@callback @callback
@ -492,7 +470,7 @@ def async_setup(hass: HomeAssistant) -> None:
async def _async_load_translations(event: Event) -> None: async def _async_load_translations(event: Event) -> None:
new_language = event.data["language"] new_language = event.data["language"]
_LOGGER.debug("Loading translations for language: %s", new_language) _LOGGER.debug("Loading translations for language: %s", new_language)
await _async_load_state_translations_to_cache(hass, new_language, None) await cache.async_load(new_language, hass.config.components)
@callback @callback
def _async_load_translations_for_component_filter(event: Event) -> bool: def _async_load_translations_for_component_filter(event: Event) -> bool:
@ -506,16 +484,17 @@ def async_setup(hass: HomeAssistant) -> None:
) )
async def _async_load_translations_for_component(event: Event) -> None: async def _async_load_translations_for_component(event: Event) -> None:
"""Load translations for a component."""
component: str | None = event.data.get("component") component: str | None = event.data.get("component")
if TYPE_CHECKING: if TYPE_CHECKING:
assert component is not None assert component is not None
language = hass.config.language language = hass.config.language
_LOGGER.debug( _LOGGER.debug(
"Loading translations for language: %s and component: %s", "Loading translations for language: %s and component: %s",
hass.config.language, language,
component, component,
) )
await _async_load_state_translations_to_cache(hass, language, component) await cache.async_load(language, {component})
hass.bus.async_listen( hass.bus.async_listen(
EVENT_COMPONENT_LOADED, EVENT_COMPONENT_LOADED,
@ -529,6 +508,13 @@ def async_setup(hass: HomeAssistant) -> None:
) )
async def async_load_integrations(hass: HomeAssistant, integrations: set[str]) -> None:
"""Load translations for integrations."""
await _async_get_translations_cache(hass).async_load(
hass.config.language, integrations
)
@callback @callback
def async_translate_state( def async_translate_state(
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -1377,7 +1377,7 @@ def mock_integration(
f"{loader.PACKAGE_BUILTIN}.{module.DOMAIN}" f"{loader.PACKAGE_BUILTIN}.{module.DOMAIN}"
if built_in if built_in
else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}", else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}",
None, pathlib.Path(""),
module.mock_manifest(), module.mock_manifest(),
) )

View File

@ -1972,7 +1972,7 @@ async def test_state_translated(
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
await translation._async_load_state_translations_to_cache(hass, "en", None) await translation._async_get_translations_cache(hass).async_load("en", set())
hass.states.async_set("switch.without_translations", "on", attributes={}) hass.states.async_set("switch.without_translations", "on", attributes={})
hass.states.async_set("binary_sensor.without_device_class", "on", attributes={}) hass.states.async_set("binary_sensor.without_device_class", "on", attributes={})

View File

@ -544,38 +544,6 @@ async def test_custom_component_translations(
assert await translation.async_get_translations(hass, "en", "state") == {} assert await translation.async_get_translations(hass, "en", "state") == {}
async def test_load_state_translations_to_cache(
hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None
):
"""Test the load state translations to cache helper."""
with patch(
"homeassistant.helpers.translation._async_load_translations",
) as mock:
await translation._async_load_state_translations_to_cache(hass, "en", None)
mock.assert_has_calls(
[
call(hass, "en", "entity", None),
call(hass, "en", "state", None),
call(hass, "en", "entity_component", None),
]
)
with patch(
"homeassistant.helpers.translation._async_load_translations",
) as mock:
await translation._async_load_state_translations_to_cache(
hass, "en", "some_integration"
)
mock.assert_has_calls(
[
call(hass, "en", "entity", "some_integration"),
call(hass, "en", "state", "some_integration"),
call(hass, "en", "entity_component", "some_integration"),
]
)
async def test_get_cached_translations( async def test_get_cached_translations(
hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None
): ):
@ -586,27 +554,33 @@ async def test_get_cached_translations(
assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}}) assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}})
await hass.async_block_till_done() await hass.async_block_till_done()
await translation._async_load_state_translations_to_cache(hass, "en", None) await translation._async_get_translations_cache(hass).async_load(
"en", hass.config.components
)
translations = translation.async_get_cached_translations(hass, "en", "state") translations = translation.async_get_cached_translations(hass, "en", "state")
assert translations["component.switch.state.string1"] == "Value 1" assert translations["component.switch.state.string1"] == "Value 1"
assert translations["component.switch.state.string2"] == "Value 2" assert translations["component.switch.state.string2"] == "Value 2"
await translation._async_load_state_translations_to_cache(hass, "de", None) await translation._async_get_translations_cache(hass).async_load(
"de", hass.config.components
)
translations = translation.async_get_cached_translations(hass, "de", "state") translations = translation.async_get_cached_translations(hass, "de", "state")
assert "component.switch.something" not in translations assert "component.switch.something" not in translations
assert translations["component.switch.state.string1"] == "German Value 1" assert translations["component.switch.state.string1"] == "German Value 1"
assert translations["component.switch.state.string2"] == "German Value 2" assert translations["component.switch.state.string2"] == "German Value 2"
# Test a partial translation # Test a partial translation
await translation._async_load_state_translations_to_cache(hass, "es", None) await translation._async_get_translations_cache(hass).async_load(
"es", hass.config.components
)
translations = translation.async_get_cached_translations(hass, "es", "state") translations = translation.async_get_cached_translations(hass, "es", "state")
assert translations["component.switch.state.string1"] == "Spanish Value 1" assert translations["component.switch.state.string1"] == "Spanish Value 1"
assert translations["component.switch.state.string2"] == "Value 2" assert translations["component.switch.state.string2"] == "Value 2"
# Test that an untranslated language falls back to English. # Test that an untranslated language falls back to English.
await translation._async_load_state_translations_to_cache( await translation._async_get_translations_cache(hass).async_load(
hass, "invalid-language", None "invalid-language", hass.config.components
) )
translations = translation.async_get_cached_translations( translations = translation.async_get_cached_translations(
hass, "invalid-language", "state" hass, "invalid-language", "state"
@ -620,14 +594,14 @@ async def test_setup(hass: HomeAssistant):
translation.async_setup(hass) translation.async_setup(hass)
with patch( with patch(
"homeassistant.helpers.translation._async_load_state_translations_to_cache", "homeassistant.helpers.translation._TranslationCache.async_load",
) as mock: ) as mock:
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "loaded_component"}) hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "loaded_component"})
await hass.async_block_till_done() await hass.async_block_till_done()
mock.assert_called_once_with(hass, hass.config.language, "loaded_component") mock.assert_called_once_with(hass.config.language, {"loaded_component"})
with patch( with patch(
"homeassistant.helpers.translation._async_load_state_translations_to_cache", "homeassistant.helpers.translation._TranslationCache.async_load",
) as mock: ) as mock:
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "config.component"}) hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "config.component"})
await hass.async_block_till_done() await hass.async_block_till_done()
@ -635,7 +609,7 @@ async def test_setup(hass: HomeAssistant):
# Should not be called if the language is the current language # Should not be called if the language is the current language
with patch( with patch(
"homeassistant.helpers.translation._async_load_state_translations_to_cache", "homeassistant.helpers.translation._TranslationCache.async_load",
) as mock: ) as mock:
hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, {"language": "en"}) hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, {"language": "en"})
await hass.async_block_till_done() await hass.async_block_till_done()
@ -643,14 +617,14 @@ async def test_setup(hass: HomeAssistant):
# Should be called if the language is different # Should be called if the language is different
with patch( with patch(
"homeassistant.helpers.translation._async_load_state_translations_to_cache", "homeassistant.helpers.translation._TranslationCache.async_load",
) as mock: ) as mock:
hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, {"language": "es"}) hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, {"language": "es"})
await hass.async_block_till_done() await hass.async_block_till_done()
mock.assert_called_once_with(hass, "es", None) mock.assert_called_once_with("es", set())
with patch( with patch(
"homeassistant.helpers.translation._async_load_state_translations_to_cache", "homeassistant.helpers.translation._TranslationCache.async_load",
) as mock: ) as mock:
hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, {}) hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, {})
await hass.async_block_till_done() await hass.async_block_till_done()