perf(registry): use insertion-ordered dicts for O(1) removal by bluetoothbot · Pull Request #1786 · python-zeroconf/python-zeroconf · GitHub
Skip to content

perf(registry): use insertion-ordered dicts for O(1) removal#1786

Draft
bluetoothbot wants to merge 2 commits into
python-zeroconf:masterfrom
bluetoothbot:koan/fix-issue-1781
Draft

perf(registry): use insertion-ordered dicts for O(1) removal#1786
bluetoothbot wants to merge 2 commits into
python-zeroconf:masterfrom
bluetoothbot:koan/fix-issue-1781

Conversation

@bluetoothbot

@bluetoothbot bluetoothbot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

ServiceRegistry._remove was O(n) per call because the per-type and per-server indices were stored as list[str], so bulk async_remove of N services sharing a type/server degraded to O(N²) — 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 so long-lived Zeroconf instances with churning type/server names don't leak dict keys.

Closes #1781

Changes

  • src/zeroconf/_services/registry.py: types and servers are now dict[str, dict[str, None]]; _add uses setdefault(...)[key] = None; _remove does del bucket[key] and cleans up the bucket when empty.
  • src/zeroconf/_services/registry.pxd: updated cython.locals to reflect the new dict-of-dicts shape.
  • tests/services/test_registry.py: added three tests pinning empty-bucket cleanup, insertion-order preservation under bulk removal, and add-after-bucket-deletion.

Test plan

  • poetry run pytest tests/services/test_registry.py -v — 8 passed (3 new tests fail without the fix).
  • poetry run pytest tests/ --timeout=60 -q — 395 passed, 2 skipped, 3 xfailed, 4 xpassed.
  • poetry run ruff check + ruff format --check on the touched files — clean.
  • cythonize src/zeroconf/_services/registry.py — compiles with no new warnings.

Generated by Kōan


Quality Report

Changes: 3 files changed, 104 insertions(+), 12 deletions(-)

Code scan: clean

Tests: failed (4 failed, 4 PASSED)

Branch hygiene: clean

Generated by Kōan post-mission quality pipeline

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.
@codecov

codecov Bot commented May 26, 2026

Copy link
Copy Markdown

@codspeed-hq

codspeed-hq Bot commented May 26, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

optimization: ServiceRegistry removal is O(n) per call because types/servers are lists

1 participant