Skip to content

Commit a4e0bdc

Browse files
committed
perf(registry): use insertion-ordered dicts for O(1) removal
ServiceRegistry's per-type and per-server indices were stored as list[str], so each list.remove(...) call in _remove was an O(n) linear scan. Bulk async_remove of N services sharing a type or server therefore degraded to O(N**2) — visible at shutdown for deployments with many entries under one _type._tcp.local. Switch the value type to dict[str, None], which preserves insertion order (so async_get_infos_type / async_get_infos_server still return entries in registration order) while giving O(1) add and remove. Also delete empty buckets once their last entry is removed so that long-lived Zeroconf instances with churning type / server names don't leak dict keys.
1 parent 279c1dd commit a4e0bdc

2 files changed

Lines changed: 27 additions & 12 deletions

File tree

src/zeroconf/_services/registry.pxd

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@ cdef class ServiceRegistry:
1212
cdef public bint has_entries
1313

1414
@cython.locals(
15-
record_list=cython.list,
15+
record_keys=cython.dict,
1616
)
1717
cdef cython.list _async_get_by_index(self, cython.dict records, str key)
1818

1919
cdef _add(self, ServiceInfo info)
2020

2121
@cython.locals(
2222
info=ServiceInfo,
23-
old_service_info=ServiceInfo
23+
old_service_info=ServiceInfo,
24+
type_bucket=cython.dict,
25+
server_bucket=cython.dict,
26+
type_key=str,
27+
server_key=str,
2428
)
2529
cdef _remove(self, cython.list infos)
2630

src/zeroconf/_services/registry.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ def __init__(
4242
) -> None:
4343
"""Create the ServiceRegistry class."""
4444
self._services: dict[str, ServiceInfo] = {}
45-
self.types: dict[str, list] = {}
46-
self.servers: dict[str, list] = {}
45+
self.types: dict[str, dict[str, None]] = {}
46+
self.servers: dict[str, dict[str, None]] = {}
4747
self.has_entries: bool = False
4848

4949
def async_add(self, info: ServiceInfo) -> None:
@@ -79,12 +79,12 @@ def async_get_infos_server(self, server: str) -> list[ServiceInfo]:
7979
"""Return all ServiceInfo matching server."""
8080
return self._async_get_by_index(self.servers, server)
8181

82-
def _async_get_by_index(self, records: dict[str, list], key: _str) -> list[ServiceInfo]:
82+
def _async_get_by_index(self, records: dict[str, dict[str, None]], key: _str) -> list[ServiceInfo]:
8383
"""Return all ServiceInfo matching the index."""
84-
record_list = records.get(key)
85-
if record_list is None:
84+
record_keys = records.get(key)
85+
if record_keys is None:
8686
return []
87-
return [self._services[name] for name in record_list]
87+
return [self._services[name] for name in record_keys]
8888

8989
def _add(self, info: ServiceInfo) -> None:
9090
"""Add a new service under the lock."""
@@ -94,8 +94,11 @@ def _add(self, info: ServiceInfo) -> None:
9494

9595
info.async_clear_cache()
9696
self._services[info.key] = info
97-
self.types.setdefault(info.type.lower(), []).append(info.key)
98-
self.servers.setdefault(info.server_key, []).append(info.key)
97+
# dict[str, None] gives O(1) add/remove while preserving insertion order
98+
# so async_get_infos_type / async_get_infos_server return entries in the
99+
# order they were registered.
100+
self.types.setdefault(info.type.lower(), {})[info.key] = None
101+
self.servers.setdefault(info.server_key, {})[info.key] = None
99102
self.has_entries = True
100103

101104
def _remove(self, infos: list[ServiceInfo]) -> None:
@@ -105,8 +108,16 @@ def _remove(self, infos: list[ServiceInfo]) -> None:
105108
if old_service_info is None:
106109
continue
107110
assert old_service_info.server_key is not None
108-
self.types[old_service_info.type.lower()].remove(info.key)
109-
self.servers[old_service_info.server_key].remove(info.key)
111+
type_key = old_service_info.type.lower()
112+
server_key = old_service_info.server_key
113+
type_bucket = self.types[type_key]
114+
del type_bucket[info.key]
115+
if not type_bucket:
116+
del self.types[type_key]
117+
server_bucket = self.servers[server_key]
118+
del server_bucket[info.key]
119+
if not server_bucket:
120+
del self.servers[server_key]
110121
del self._services[info.key]
111122

112123
self.has_entries = bool(self._services)

0 commit comments

Comments
 (0)