Speed up entity filter when there are many glob matchers (#90615)

* Speed up entity filter when there are many glob matchers

Since we do no care about which glob matches we can
combine all the translated globs into a single regex
which reduces the overhead

* delete unused code

* preen
This commit is contained in:
J. Nick Koston 2023-03-31 15:18:29 -10:00 committed by GitHub
parent 3e94f2a502
commit 44b35fea47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 29 additions and 36 deletions

View File

@ -33,26 +33,20 @@ class EntityFilter:
self._exclude_e = set(config[CONF_EXCLUDE_ENTITIES])
self._include_d = set(config[CONF_INCLUDE_DOMAINS])
self._exclude_d = set(config[CONF_EXCLUDE_DOMAINS])
self._include_eg = _convert_globs_to_pattern_list(
config[CONF_INCLUDE_ENTITY_GLOBS]
)
self._exclude_eg = _convert_globs_to_pattern_list(
config[CONF_EXCLUDE_ENTITY_GLOBS]
)
self._include_eg = _convert_globs_to_pattern(config[CONF_INCLUDE_ENTITY_GLOBS])
self._exclude_eg = _convert_globs_to_pattern(config[CONF_EXCLUDE_ENTITY_GLOBS])
self._filter: Callable[[str], bool] | None = None
def explicitly_included(self, entity_id: str) -> bool:
"""Check if an entity is explicitly included."""
return entity_id in self._include_e or (
bool(self._include_eg)
and _test_against_patterns(self._include_eg, entity_id)
bool(self._include_eg and self._include_eg.match(entity_id))
)
def explicitly_excluded(self, entity_id: str) -> bool:
"""Check if an entity is explicitly excluded."""
return entity_id in self._exclude_e or (
bool(self._exclude_eg)
and _test_against_patterns(self._exclude_eg, entity_id)
bool(self._exclude_eg and self._exclude_eg.match(entity_id))
)
def __call__(self, entity_id: str) -> bool:
@ -140,19 +134,22 @@ INCLUDE_EXCLUDE_FILTER_SCHEMA = vol.All(
)
def _glob_to_re(glob: str) -> re.Pattern[str]:
"""Translate and compile glob string into pattern."""
return re.compile(fnmatch.translate(glob))
def _test_against_patterns(patterns: list[re.Pattern[str]], entity_id: str) -> bool:
"""Test entity against list of patterns, true if any match."""
return any(pattern.match(entity_id) for pattern in patterns)
def _convert_globs_to_pattern_list(globs: list[str] | None) -> list[re.Pattern[str]]:
def _convert_globs_to_pattern(globs: list[str] | None) -> re.Pattern[str] | None:
"""Convert a list of globs to a re pattern list."""
return list(map(_glob_to_re, set(globs or [])))
if globs is None:
return None
translated_patterns: list[str] = []
for glob in set(globs):
if pattern := fnmatch.translate(glob):
translated_patterns.append(pattern)
if not translated_patterns:
return None
inner = "|".join(translated_patterns)
combined = f"(?:{inner})"
return re.compile(combined)
def generate_filter(
@ -169,8 +166,8 @@ def generate_filter(
set(include_entities),
set(exclude_domains),
set(exclude_entities),
_convert_globs_to_pattern_list(include_entity_globs),
_convert_globs_to_pattern_list(exclude_entity_globs),
_convert_globs_to_pattern(include_entity_globs),
_convert_globs_to_pattern(exclude_entity_globs),
)
@ -179,8 +176,8 @@ def _generate_filter_from_sets_and_pattern_lists(
include_e: set[str],
exclude_d: set[str],
exclude_e: set[str],
include_eg: list[re.Pattern[str]],
exclude_eg: list[re.Pattern[str]],
include_eg: re.Pattern[str] | None,
exclude_eg: re.Pattern[str] | None,
) -> Callable[[str], bool]:
"""Generate a filter from pre-comuted sets and pattern lists."""
have_exclude = bool(exclude_e or exclude_d or exclude_eg)
@ -191,7 +188,7 @@ def _generate_filter_from_sets_and_pattern_lists(
return (
entity_id in include_e
or domain in include_d
or (bool(include_eg) and _test_against_patterns(include_eg, entity_id))
or (bool(include_eg and include_eg.match(entity_id)))
)
def entity_excluded(domain: str, entity_id: str) -> bool:
@ -199,7 +196,7 @@ def _generate_filter_from_sets_and_pattern_lists(
return (
entity_id in exclude_e
or domain in exclude_d
or (bool(exclude_eg) and _test_against_patterns(exclude_eg, entity_id))
or (bool(exclude_eg and exclude_eg.match(entity_id)))
)
# Case 1 - No filter
@ -249,12 +246,10 @@ def _generate_filter_from_sets_and_pattern_lists(
return entity_id in include_e or (
entity_id not in exclude_e
and (
(include_eg and _test_against_patterns(include_eg, entity_id))
bool(include_eg and include_eg.match(entity_id))
or (
split_entity_id(entity_id)[0] in include_d
and not (
exclude_eg and _test_against_patterns(exclude_eg, entity_id)
)
and not (exclude_eg and exclude_eg.match(entity_id))
)
)
)
@ -272,9 +267,7 @@ def _generate_filter_from_sets_and_pattern_lists(
def entity_filter_4b(entity_id: str) -> bool:
"""Return filter function for case 4b."""
domain = split_entity_id(entity_id)[0]
if domain in exclude_d or (
exclude_eg and _test_against_patterns(exclude_eg, entity_id)
):
if domain in exclude_d or bool(exclude_eg and exclude_eg.match(entity_id)):
return entity_id in include_e
return entity_id not in exclude_e

View File

@ -369,7 +369,7 @@ def test_filter_schema_include_exclude() -> None:
assert not filt.empty_filter
def test_exlictly_included() -> None:
def test_explicitly_included() -> None:
"""Test if an entity is explicitly included."""
conf = {
"include": {