Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: performance regression with ServiceInfo IPv6Addresses (#1330)
- Loading branch information
Showing
6 changed files
with
180 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
cdef bint TYPE_CHECKING | ||
cdef bint IPADDRESS_SUPPORTS_SCOPE_ID | ||
|
||
from .._dns cimport DNSAddress | ||
|
||
|
||
cpdef get_ip_address_object_from_record(DNSAddress record) | ||
|
||
@cython.locals(address_str=str) | ||
cpdef str_without_scope_id(object addr) | ||
|
||
cpdef ip_bytes_and_scope_to_address(object addr, object scope_id) | ||
|
||
cdef object cached_ip_addresses_wrapper |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine | ||
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine | ||
This module provides a framework for the use of DNS Service Discovery | ||
using IP multicast. | ||
This library is free software; you can redistribute it and/or | ||
modify it under the terms of the GNU Lesser General Public | ||
License as published by the Free Software Foundation; either | ||
version 2.1 of the License, or (at your option) any later version. | ||
This library is distributed in the hope that it will be useful, | ||
but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
Lesser General Public License for more details. | ||
You should have received a copy of the GNU Lesser General Public | ||
License along with this library; if not, write to the Free Software | ||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 | ||
USA | ||
""" | ||
import sys | ||
from functools import lru_cache | ||
from ipaddress import AddressValueError, IPv4Address, IPv6Address, NetmaskValueError | ||
from typing import Any, Optional, Union | ||
|
||
from .._dns import DNSAddress | ||
from ..const import _TYPE_AAAA | ||
|
||
bytes_ = bytes | ||
int_ = int | ||
IPADDRESS_SUPPORTS_SCOPE_ID = sys.version_info >= (3, 9, 0) | ||
|
||
|
||
class ZeroconfIPv4Address(IPv4Address): | ||
|
||
__slots__ = ("_str", "_is_link_local") | ||
|
||
def __init__(self, *args: Any, **kwargs: Any) -> None: | ||
"""Initialize a new IPv4 address.""" | ||
super().__init__(*args, **kwargs) | ||
self._str = super().__str__() | ||
self._is_link_local = super().is_link_local | ||
|
||
def __str__(self) -> str: | ||
"""Return the string representation of the IPv4 address.""" | ||
return self._str | ||
|
||
@property | ||
def is_link_local(self) -> bool: | ||
"""Return True if this is a link-local address.""" | ||
return self._is_link_local | ||
|
||
|
||
class ZeroconfIPv6Address(IPv6Address): | ||
|
||
__slots__ = ("_str", "_is_link_local") | ||
|
||
def __init__(self, *args: Any, **kwargs: Any) -> None: | ||
"""Initialize a new IPv6 address.""" | ||
super().__init__(*args, **kwargs) | ||
self._str = super().__str__() | ||
self._is_link_local = super().is_link_local | ||
|
||
def __str__(self) -> str: | ||
"""Return the string representation of the IPv6 address.""" | ||
return self._str | ||
|
||
@property | ||
def is_link_local(self) -> bool: | ||
"""Return True if this is a link-local address.""" | ||
return self._is_link_local | ||
|
||
|
||
@lru_cache(maxsize=512) | ||
def _cached_ip_addresses(address: Union[str, bytes, int]) -> Optional[Union[IPv4Address, IPv6Address]]: | ||
"""Cache IP addresses.""" | ||
try: | ||
return ZeroconfIPv4Address(address) | ||
except (AddressValueError, NetmaskValueError): | ||
pass | ||
|
||
try: | ||
return ZeroconfIPv6Address(address) | ||
except (AddressValueError, NetmaskValueError): | ||
return None | ||
|
||
|
||
cached_ip_addresses_wrapper = _cached_ip_addresses | ||
cached_ip_addresses = cached_ip_addresses_wrapper | ||
|
||
|
||
def get_ip_address_object_from_record(record: DNSAddress) -> Optional[Union[IPv4Address, IPv6Address]]: | ||
"""Get the IP address object from the record.""" | ||
if IPADDRESS_SUPPORTS_SCOPE_ID and record.type == _TYPE_AAAA and record.scope_id is not None: | ||
return ip_bytes_and_scope_to_address(record.address, record.scope_id) | ||
return cached_ip_addresses_wrapper(record.address) | ||
|
||
|
||
def ip_bytes_and_scope_to_address(address: bytes_, scope: int_) -> Optional[Union[IPv4Address, IPv6Address]]: | ||
"""Convert the bytes and scope to an IP address object.""" | ||
base_address = cached_ip_addresses_wrapper(address) | ||
if base_address is not None and base_address.is_link_local: | ||
return cached_ip_addresses_wrapper(f"{base_address}%{scope}") | ||
return base_address | ||
|
||
|
||
def str_without_scope_id(addr: Union[IPv4Address, IPv6Address]) -> str: | ||
"""Return the string representation of the address without the scope id.""" | ||
if IPADDRESS_SUPPORTS_SCOPE_ID and addr.version == 6: | ||
address_str = str(addr) | ||
return address_str.partition('%')[0] | ||
return str(addr) | ||
|
||
|
||
__all__ = ( | ||
"cached_ip_addresses", | ||
"get_ip_address_object_from_record", | ||
"ip_bytes_and_scope_to_address", | ||
"str_without_scope_id", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
#!/usr/bin/env python | ||
|
||
"""Unit tests for zeroconf._utils.ipaddress.""" | ||
|
||
from zeroconf._utils import ipaddress | ||
|
||
|
||
def test_cached_ip_addresses_wrapper(): | ||
"""Test the cached_ip_addresses_wrapper.""" | ||
assert ipaddress.cached_ip_addresses('') is None | ||
assert ipaddress.cached_ip_addresses('foo') is None | ||
assert ( | ||
str(ipaddress.cached_ip_addresses(b'&\x06(\x00\x02 \x00\x01\x02H\x18\x93%\xc8\x19F')) | ||
== '2606:2800:220:1:248:1893:25c8:1946' | ||
) | ||
assert ipaddress.cached_ip_addresses('::1') == ipaddress.IPv6Address('::1') | ||
|
||
ipv4 = ipaddress.cached_ip_addresses('169.254.0.0') | ||
assert ipv4 is not None | ||
assert ipv4.is_link_local is True | ||
|
||
ipv6 = ipaddress.cached_ip_addresses('fe80::1') | ||
assert ipv6 is not None | ||
assert ipv6.is_link_local is True |