From 433dd409f8dcf29329ea31e9a14cf57729f94bc4 Mon Sep 17 00:00:00 2001 From: Bluetooth Devices Bot Date: Sun, 24 May 2026 19:07:23 +0000 Subject: [PATCH 1/2] test: add benchmarks for ipaddress object creation and hashing --- tests/benchmarks/test_ipaddress.py | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/benchmarks/test_ipaddress.py diff --git a/tests/benchmarks/test_ipaddress.py b/tests/benchmarks/test_ipaddress.py new file mode 100644 index 00000000..60aea89c --- /dev/null +++ b/tests/benchmarks/test_ipaddress.py @@ -0,0 +1,48 @@ +"""Benchmarks for zeroconf._utils.ipaddress address objects.""" + +from __future__ import annotations + +from pytest_codspeed import BenchmarkFixture + +from zeroconf._utils.ipaddress import ZeroconfIPv4Address, ZeroconfIPv6Address + +_IPV4_STRS = [f"10.{(i >> 8) & 0xFF}.{i & 0xFF}.1" for i in range(1000)] +_IPV6_BYTES = [(0x20010DB8 << 96 | i).to_bytes(16, "big") for i in range(1000)] + + +def test_create_ipv4_addresses(benchmark: BenchmarkFixture) -> None: + """Benchmark constructing 1000 distinct IPv4 address objects.""" + + @benchmark + def _create() -> None: + for addr in _IPV4_STRS: + ZeroconfIPv4Address(addr) + + +def test_create_ipv6_addresses(benchmark: BenchmarkFixture) -> None: + """Benchmark constructing 1000 distinct IPv6 address objects.""" + + @benchmark + def _create() -> None: + for addr in _IPV6_BYTES: + ZeroconfIPv6Address(addr) + + +def test_hash_ipv4_address(benchmark: BenchmarkFixture) -> None: + """Benchmark hashing the same IPv4 address object 1000 times.""" + addr = ZeroconfIPv4Address("10.0.0.1") + + @benchmark + def _hash() -> None: + for _ in range(1000): + hash(addr) + + +def test_hash_ipv6_address(benchmark: BenchmarkFixture) -> None: + """Benchmark hashing the same IPv6 address object 1000 times.""" + addr = ZeroconfIPv6Address(b"\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01") + + @benchmark + def _hash() -> None: + for _ in range(1000): + hash(addr) From 1a232c472bad58783c55cb376e74866caf605699 Mon Sep 17 00:00:00 2001 From: Bluetooth Devices Bot Date: Sun, 24 May 2026 19:09:38 +0000 Subject: [PATCH 2/2] perf: precompute address hash instead of per-instance lru_cache ZeroconfIPv4Address/ZeroconfIPv6Address assigned self.__hash__ to a functools.cache(lambda ...) in __init__. Because __hash__ sat in __slots__ the memoization did work, but every address object paid to build a full lru_cache wrapper plus a self-capturing lambda at construction time (and a self -> slot -> wrapper -> lambda -> self reference cycle for the GC to reap). Compute the hash once into a plain int slot and expose it through a real __hash__ method. Construction drops ~25-45% in pure-Python micro- benchmarks; hash() stays memoized via the precomputed slot. --- src/zeroconf/_utils/ipaddress.py | 18 +++++++++++++----- tests/utils/test_ipaddress.py | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/zeroconf/_utils/ipaddress.py b/src/zeroconf/_utils/ipaddress.py index d172d0c9..c37bfe7e 100644 --- a/src/zeroconf/_utils/ipaddress.py +++ b/src/zeroconf/_utils/ipaddress.py @@ -22,7 +22,7 @@ from __future__ import annotations -from functools import cache, lru_cache +from functools import lru_cache from ipaddress import AddressValueError, IPv4Address, IPv6Address, NetmaskValueError from typing import Any @@ -34,7 +34,7 @@ class ZeroconfIPv4Address(IPv4Address): - __slots__ = ("__hash__", "_is_link_local", "_is_loopback", "_is_unspecified", "_str", "zc_integer") + __slots__ = ("_hash", "_is_link_local", "_is_loopback", "_is_unspecified", "_str", "zc_integer") def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize a new IPv4 address.""" @@ -43,13 +43,17 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self._is_link_local = super().is_link_local self._is_unspecified = super().is_unspecified self._is_loopback = super().is_loopback - self.__hash__ = cache(lambda: IPv4Address.__hash__(self)) # type: ignore[method-assign] + self._hash = IPv4Address.__hash__(self) self.zc_integer = int(self) def __str__(self) -> str: """Return the string representation of the IPv4 address.""" return self._str + def __hash__(self) -> int: + """Return the precomputed hash of the IPv4 address.""" + return self._hash + @property def is_link_local(self) -> bool: """Return True if this is a link-local address.""" @@ -67,7 +71,7 @@ def is_loopback(self) -> bool: class ZeroconfIPv6Address(IPv6Address): - __slots__ = ("__hash__", "_is_link_local", "_is_loopback", "_is_unspecified", "_str", "zc_integer") + __slots__ = ("_hash", "_is_link_local", "_is_loopback", "_is_unspecified", "_str", "zc_integer") def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize a new IPv6 address.""" @@ -76,13 +80,17 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self._is_link_local = super().is_link_local self._is_unspecified = super().is_unspecified self._is_loopback = super().is_loopback - self.__hash__ = cache(lambda: IPv6Address.__hash__(self)) # type: ignore[method-assign] + self._hash = IPv6Address.__hash__(self) self.zc_integer = int(self) def __str__(self) -> str: """Return the string representation of the IPv6 address.""" return self._str + def __hash__(self) -> int: + """Return the precomputed hash of the IPv6 address.""" + return self._hash + @property def is_link_local(self) -> bool: """Return True if this is a link-local address.""" diff --git a/tests/utils/test_ipaddress.py b/tests/utils/test_ipaddress.py index 4379f458..975b07f6 100644 --- a/tests/utils/test_ipaddress.py +++ b/tests/utils/test_ipaddress.py @@ -48,6 +48,20 @@ def test_cached_ip_addresses_wrapper(): assert ipv6.is_unspecified is True +def test_address_hash_matches_stdlib_and_dedups(): + """Cached address objects hash like their stdlib equals and dedup in sets.""" + v4 = ipaddress.cached_ip_addresses("192.168.1.1") + assert v4 is not None + assert hash(v4) == hash(ipaddress.IPv4Address("192.168.1.1")) + assert hash(v4) == hash(v4) + assert len({v4, ipaddress.ZeroconfIPv4Address("192.168.1.1")}) == 1 + + v6 = ipaddress.cached_ip_addresses("fe80::1") + assert v6 is not None + assert hash(v6) == hash(ipaddress.IPv6Address("fe80::1")) + assert len({v6, ipaddress.ZeroconfIPv6Address("fe80::1")}) == 1 + + def test_get_ip_address_object_from_record(): """Test the get_ip_address_object_from_record.""" # not link local