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._exclude_e = set(config[CONF_EXCLUDE_ENTITIES])
self._include_d = set(config[CONF_INCLUDE_DOMAINS]) self._include_d = set(config[CONF_INCLUDE_DOMAINS])
self._exclude_d = set(config[CONF_EXCLUDE_DOMAINS]) self._exclude_d = set(config[CONF_EXCLUDE_DOMAINS])
self._include_eg = _convert_globs_to_pattern_list( self._include_eg = _convert_globs_to_pattern(config[CONF_INCLUDE_ENTITY_GLOBS])
config[CONF_INCLUDE_ENTITY_GLOBS] self._exclude_eg = _convert_globs_to_pattern(config[CONF_EXCLUDE_ENTITY_GLOBS])
)
self._exclude_eg = _convert_globs_to_pattern_list(
config[CONF_EXCLUDE_ENTITY_GLOBS]
)
self._filter: Callable[[str], bool] | None = None self._filter: Callable[[str], bool] | None = None
def explicitly_included(self, entity_id: str) -> bool: def explicitly_included(self, entity_id: str) -> bool:
"""Check if an entity is explicitly included.""" """Check if an entity is explicitly included."""
return entity_id in self._include_e or ( return entity_id in self._include_e or (
bool(self._include_eg) bool(self._include_eg and self._include_eg.match(entity_id))
and _test_against_patterns(self._include_eg, entity_id)
) )
def explicitly_excluded(self, entity_id: str) -> bool: def explicitly_excluded(self, entity_id: str) -> bool:
"""Check if an entity is explicitly excluded.""" """Check if an entity is explicitly excluded."""
return entity_id in self._exclude_e or ( return entity_id in self._exclude_e or (
bool(self._exclude_eg) bool(self._exclude_eg and self._exclude_eg.match(entity_id))
and _test_against_patterns(self._exclude_eg, entity_id)
) )
def __call__(self, entity_id: str) -> bool: 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]: def _convert_globs_to_pattern(globs: list[str] | None) -> re.Pattern[str] | None:
"""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]]:
"""Convert a list of globs to a re pattern list.""" """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( def generate_filter(
@ -169,8 +166,8 @@ def generate_filter(
set(include_entities), set(include_entities),
set(exclude_domains), set(exclude_domains),
set(exclude_entities), set(exclude_entities),
_convert_globs_to_pattern_list(include_entity_globs), _convert_globs_to_pattern(include_entity_globs),
_convert_globs_to_pattern_list(exclude_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], include_e: set[str],
exclude_d: set[str], exclude_d: set[str],
exclude_e: set[str], exclude_e: set[str],
include_eg: list[re.Pattern[str]], include_eg: re.Pattern[str] | None,
exclude_eg: list[re.Pattern[str]], exclude_eg: re.Pattern[str] | None,
) -> Callable[[str], bool]: ) -> Callable[[str], bool]:
"""Generate a filter from pre-comuted sets and pattern lists.""" """Generate a filter from pre-comuted sets and pattern lists."""
have_exclude = bool(exclude_e or exclude_d or exclude_eg) have_exclude = bool(exclude_e or exclude_d or exclude_eg)
@ -191,7 +188,7 @@ def _generate_filter_from_sets_and_pattern_lists(
return ( return (
entity_id in include_e entity_id in include_e
or domain in include_d 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: def entity_excluded(domain: str, entity_id: str) -> bool:
@ -199,7 +196,7 @@ def _generate_filter_from_sets_and_pattern_lists(
return ( return (
entity_id in exclude_e entity_id in exclude_e
or domain in exclude_d 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 # Case 1 - No filter
@ -249,12 +246,10 @@ def _generate_filter_from_sets_and_pattern_lists(
return entity_id in include_e or ( return entity_id in include_e or (
entity_id not in exclude_e entity_id not in exclude_e
and ( and (
(include_eg and _test_against_patterns(include_eg, entity_id)) bool(include_eg and include_eg.match(entity_id))
or ( or (
split_entity_id(entity_id)[0] in include_d split_entity_id(entity_id)[0] in include_d
and not ( and not (exclude_eg and exclude_eg.match(entity_id))
exclude_eg and _test_against_patterns(exclude_eg, entity_id)
)
) )
) )
) )
@ -272,9 +267,7 @@ def _generate_filter_from_sets_and_pattern_lists(
def entity_filter_4b(entity_id: str) -> bool: def entity_filter_4b(entity_id: str) -> bool:
"""Return filter function for case 4b.""" """Return filter function for case 4b."""
domain = split_entity_id(entity_id)[0] domain = split_entity_id(entity_id)[0]
if domain in exclude_d or ( if domain in exclude_d or bool(exclude_eg and exclude_eg.match(entity_id)):
exclude_eg and _test_against_patterns(exclude_eg, entity_id)
):
return entity_id in include_e return entity_id in include_e
return entity_id not in exclude_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 assert not filt.empty_filter
def test_exlictly_included() -> None: def test_explicitly_included() -> None:
"""Test if an entity is explicitly included.""" """Test if an entity is explicitly included."""
conf = { conf = {
"include": { "include": {