perf(registry): use insertion-ordered dicts for O(1) removal - #1786
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1786 +/- ##
=======================================
Coverage 99.81% 99.81%
=======================================
Files 33 33
Lines 3759 3768 +9
Branches 533 535 +2
=======================================
+ Hits 3752 3761 +9
Misses 5 5
Partials 2 2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merging this PR will not alter performance
Performance Changes
Comparing |
|
right fix and we only support newer than python 3.7 so order is guarenteed |
Confirmed. One extra behaviour change beyond ordering: empty-bucket cleanup means |
|
|
Rebase with requested adjustmentsBranch Changes applied
Not changed (and why)
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
a4e0bdc to
1a07182
Compare
|
|
Rebase with requested adjustmentsBranch Changes applied
Not changed (and why)
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
|
|
Rebase with requested adjustmentsBranch Changes applied
Not changed (and why)
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
|
|
1c08ff2 to
5643238
Compare
Rebase with requested adjustmentsBranch Warning Force-push safety check — the rewrite of Commits whose patch changed in-flight and whose content could not be verified as preserved — check these survived:
Previous PR head: Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
|
|
Rebase with requested adjustmentsBranch Changes applied
Not changed (and why)
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
|
Note No code changes since the last review ( |
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.
5643238 to
47d9273
Compare
Rebase with requested adjustmentsBranch Changes applied
Not changed (and why)
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
PR Review — perf(registry): use insertion-ordered dicts for O(1) removalMerge-ready. Every finding from the prior round is resolved; I found nothing new. The core change is tight and the invariants hold under inspection. Empty-bucket deletion is the one risky part and it is safe: the
Checklist
Automated review by Kōan (Claude) |
Rebase with requested adjustmentsBranch Changes applied
Not changed (and why)
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
Summary
ServiceRegistry._removewas O(n) per call because the per-type and per-server indices were stored aslist[str], so bulkasync_removeof 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 adictkeyed byinfo.key, which preserves insertion order (soasync_get_infos_type/async_get_infos_serverstill return entries in registration order) while giving O(1) add and remove. The bucket values are theServiceInfoobjects themselves, so the read path returnslist(bucket.values())with no second lookup into_services. Also delete empty buckets so long-livedZeroconfinstances with churning type/server names don't leak dict keys.Closes #1781
Changes
src/zeroconf/_services/registry.py:typesandserversare nowdict[str, dict[str, ServiceInfo]](aliased_ServiceIndex);_adddoessetdefault(...)[info.key] = info;_removedoesdel bucket[info.key]and cleans up the bucket when empty.src/zeroconf/_services/registry.pxd: updatedcython.localsto reflect the new dict-of-dicts shape.tests/services/test_registry.py: added four tests covering empty-bucket cleanup, insertion-order preservation under bulk removal, add-after-bucket-deletion, and thatasync_updatereplaces the indexed object.tests/benchmarks/test_registry.py: new CodSpeed benchmarks for bulk add, bulk remove, and read-back of 500 services sharing one type/server, so both the complexity change and the read-path constant are visible in CI.Observable changes for downstream consumers
ServiceRegistryis re-exported fromzeroconffor backwards compat, so two changes are visible:registry.types[t]/registry.servers[s]are nowdict[str, ServiceInfo]instead oflist[str]. Iterating a bucket still yieldsinfo.keystrings in registration order, exactly like the oldlist[str], sofor name in registry.types[t]keeps working unchanged;len()andinare also unchanged. Only positional indexing ([0]) and mutation via.append()break.async_get_types()no longer reports a type whose last instance was unregistered. This is wire-visible:async_get_types()feeds the_services._dns-sd._udp.local.service-type enumeration response, and RFC 6763 §9 defines those PTR records as the set of service types "currently registered" / present on the network — a type with zero live instances should not be advertised. The previous behaviour advertised such types until theZeroconfinstance was torn down.Nothing in-tree depends on either behaviour.
Test plan
poetry run pytest tests/services/test_registry.py -v— 9 passed. The two bucket-cleanup tests fail without the fix;test_bulk_remove_preserves_order_of_survivorspasses onmastertoo and is a regression guard against swapping the bucket for aset.poetry run pytest tests/ --timeout=60 -q— 395 passed, 2 skipped, 3 xfailed, 4 xpassed.poetry run ruff check+ruff format --checkon the touched files — clean.cythonize src/zeroconf/_services/registry.py— compiles with no new warnings.Generated by Kōan