Skip to content

Commit 93a9efa

Browse files
authored
Reduce string conversion in CaseInsensitiveDict lookups (#173)
* Implement get on CaseInsensitiveDict get was previously provided by the parent class which had to raise KeyError for missing values. Since try/except is only cheap for the non-exception case the performance was not good when the key was missing similar to python/cpython#106665 but in the HA case we call this even more frequently
1 parent 88c4b06 commit 93a9efa

File tree

4 files changed

+48
-41
lines changed

4 files changed

+48
-41
lines changed

async_upnp_client/advertisement.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,24 +66,26 @@ def __init__(
6666

6767
def _on_data(self, request_line: str, headers: CaseInsensitiveDict) -> None:
6868
"""Handle data."""
69-
if headers.get("MAN") == SSDP_DISCOVER:
69+
if headers.get_lower("man") == SSDP_DISCOVER:
7070
# Ignore discover packets.
7171
return
72-
if "NTS" not in headers:
72+
73+
notification_sub_type = headers.get_lower("nts")
74+
if notification_sub_type is None:
7375
_LOGGER.debug("Got non-advertisement packet: %s, %s", request_line, headers)
7476
return
7577

76-
_LOGGER.debug(
77-
"Received advertisement, _remote_addr: %s, NT: %s, NTS: %s, USN: %s, location: %s",
78-
headers.get("_remote_addr", ""),
79-
headers.get("NT", "<no NT>"),
80-
headers.get("NTS", "<no NTS>"),
81-
headers.get("USN", "<no USN>"),
82-
headers.get("location", ""),
83-
)
78+
if _LOGGER.isEnabledFor(logging.DEBUG):
79+
_LOGGER.debug(
80+
"Received advertisement, _remote_addr: %s, NT: %s, NTS: %s, USN: %s, location: %s",
81+
headers.get_lower("_remote_addr", ""),
82+
headers.get_lower("nt", "<no NT>"),
83+
headers.get_lower("nts", "<no NTS>"),
84+
headers.get_lower("usn", "<no USN>"),
85+
headers.get_lower("location", ""),
86+
)
8487

8588
headers["_source"] = SsdpSource.ADVERTISEMENT
86-
notification_sub_type = headers["NTS"]
8789
if notification_sub_type == NotificationSubType.SSDP_ALIVE:
8890
if self.async_on_alive:
8991
coro = self.async_on_alive(headers)

async_upnp_client/server.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -482,16 +482,20 @@ def _on_data(
482482
# pylint: disable=too-many-branches
483483
assert self._transport
484484

485-
if request_line != "M-SEARCH * HTTP/1.1" or headers.get("MAN") != SSDP_DISCOVER:
485+
if (
486+
request_line != "M-SEARCH * HTTP/1.1"
487+
or headers.get_lower("man") != SSDP_DISCOVER
488+
):
486489
return
487490

488-
remote_addr = headers["_remote_addr"]
491+
remote_addr = headers.get_lower("_remote_addr")
489492
_LOGGER.debug("Received M-SEARCH from: %s, headers: %s", remote_addr, headers)
490493

491494
loop = asyncio.get_running_loop()
492-
if "MX" in headers:
495+
mx_header = headers.get_lower("mx")
496+
if mx_header is not None:
493497
try:
494-
delay = int(headers["MX"])
498+
delay = int(mx_header)
495499
_LOGGER.debug("Deferring response for %d seconds", delay)
496500
except ValueError:
497501
delay = 0
@@ -501,8 +505,9 @@ def _on_data(
501505

502506
def _deferred_on_data(self, headers: CaseInsensitiveDict) -> None:
503507
# Determine how we should respond, page 1.3.2 of UPnP-arch-DeviceArchitecture-v2.0.
504-
remote_addr = headers["_remote_addr"]
505-
search_target = headers["st"].lower()
508+
remote_addr = headers.get_lower("_remote_addr")
509+
st_header: str = headers.get_lower("st", "")
510+
search_target = st_header.lower()
506511
if search_target == SSDP_ST_ALL:
507512
# 3 + 2d + k (d: embedded device, k: service)
508513
# global: ST: upnp:rootdevice

async_upnp_client/ssdp_listener.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@
4141
def valid_search_headers(headers: CaseInsensitiveDict) -> bool:
4242
"""Validate if this search is usable."""
4343
# pylint: disable=invalid-name
44-
udn = headers.get("_udn") # type: Optional[str]
45-
st = headers.get("st") # type: Optional[str]
46-
location = headers.get("location", "") # type: str
44+
udn = headers.get_lower("_udn") # type: Optional[str]
45+
st = headers.get_lower("st") # type: Optional[str]
46+
location = headers.get_lower("location", "") # type: str
4747
return bool(
4848
udn
4949
and st
@@ -60,10 +60,10 @@ def valid_search_headers(headers: CaseInsensitiveDict) -> bool:
6060
def valid_advertisement_headers(headers: CaseInsensitiveDict) -> bool:
6161
"""Validate if this advertisement is usable for connecting to a device."""
6262
# pylint: disable=invalid-name
63-
udn = headers.get("_udn") # type: Optional[str]
64-
nt = headers.get("nt") # type: Optional[str]
65-
nts = headers.get("nts") # type: Optional[str]
66-
location = headers.get("location", "") # type: str
63+
udn = headers.get_lower("_udn") # type: Optional[str]
64+
nt = headers.get_lower("nt") # type: Optional[str]
65+
nts = headers.get_lower("nts") # type: Optional[str]
66+
location = headers.get_lower("location", "") # type: str
6767
return bool(
6868
udn
6969
and nt
@@ -81,22 +81,22 @@ def valid_advertisement_headers(headers: CaseInsensitiveDict) -> bool:
8181
def valid_byebye_headers(headers: CaseInsensitiveDict) -> bool:
8282
"""Validate if this advertisement has required headers for byebye."""
8383
# pylint: disable=invalid-name
84-
udn = headers.get("_udn") # type: Optional[str]
85-
nt = headers.get("nt") # type: Optional[str]
86-
nts = headers.get("nts") # type: Optional[str]
84+
udn = headers.get_lower("_udn") # type: Optional[str]
85+
nt = headers.get_lower("nt") # type: Optional[str]
86+
nts = headers.get_lower("nts") # type: Optional[str]
8787
return bool(udn and nt and nts)
8888

8989

9090
def extract_valid_to(headers: CaseInsensitiveDict) -> datetime:
9191
"""Extract/create valid to."""
92-
cache_control = headers.get("cache-control", "")
92+
cache_control = headers.get_lower("cache-control", "")
9393
match = CACHE_CONTROL_RE.search(cache_control)
9494
if match:
9595
max_age = int(match[1])
9696
uncache_after = timedelta(seconds=max_age)
9797
else:
9898
uncache_after = DEFAULT_MAX_AGE
99-
timestamp: datetime = headers["_timestamp"]
99+
timestamp: datetime = headers.get_lower("_timestamp")
100100
return timestamp + uncache_after
101101

102102

@@ -247,7 +247,7 @@ def ip_version_from_location(location: str) -> Optional[int]:
247247

248248
def location_changed(ssdp_device: SsdpDevice, headers: CaseInsensitiveDict) -> bool:
249249
"""Test if location changed for device."""
250-
new_location = headers.get("location", "")
250+
new_location = headers.get_lower("location", "")
251251
if not new_location:
252252
return False
253253

@@ -295,14 +295,14 @@ def see_search(
295295
_LOGGER.debug("Received invalid search headers: %s", headers)
296296
return False, None, None, None
297297

298-
udn = headers["_udn"]
298+
udn = headers.get_lower("_udn")
299299
is_new_device = udn not in self.devices
300300

301301
ssdp_device, new_location = self._see_device(headers)
302302
if not ssdp_device:
303303
return False, None, None, None
304304

305-
search_target: SearchTarget = headers["ST"]
305+
search_target: SearchTarget = headers.get_lower("st")
306306
is_new_service = (
307307
search_target not in ssdp_device.advertisement_headers
308308
and search_target not in ssdp_device.search_headers
@@ -339,14 +339,14 @@ def see_advertisement(
339339
_LOGGER.debug("Received invalid advertisement headers: %s", headers)
340340
return False, None, None
341341

342-
udn = headers["_udn"]
342+
udn = headers.get_lower("_udn")
343343
is_new_device = udn not in self.devices
344344

345345
ssdp_device, new_location = self._see_device(headers)
346346
if not ssdp_device:
347347
return False, None, None
348348

349-
notification_type: NotificationType = headers["NT"]
349+
notification_type: NotificationType = headers.get_lower("nt")
350350
is_new_service = (
351351
notification_type not in ssdp_device.advertisement_headers
352352
and notification_type not in ssdp_device.search_headers
@@ -356,7 +356,7 @@ def see_advertisement(
356356
"See new service: %s, type: %s", ssdp_device, notification_type
357357
)
358358

359-
notification_sub_type: NotificationSubType = headers["NTS"]
359+
notification_sub_type: NotificationSubType = headers.get_lower("nts")
360360
propagate = (
361361
notification_sub_type == NotificationSubType.SSDP_UPDATE
362362
or is_new_device
@@ -407,8 +407,8 @@ def _see_device(
407407
new_location = location_changed(ssdp_device, headers)
408408

409409
# Update device.
410-
ssdp_device.add_location(headers["location"], valid_to)
411-
ssdp_device.last_seen = headers["_timestamp"]
410+
ssdp_device.add_location(headers.get_lower("location"), valid_to)
411+
ssdp_device.last_seen = headers.get_lower("_timestamp")
412412
if not self.next_valid_to or self.next_valid_to > ssdp_device.valid_to:
413413
self.next_valid_to = ssdp_device.valid_to
414414

@@ -433,7 +433,7 @@ def unsee_advertisement(
433433
del self.devices[udn]
434434

435435
# Update device before propagating it
436-
notification_type: NotificationType = headers["NT"]
436+
notification_type: NotificationType = headers.get_lower("nt")
437437
if notification_type in ssdp_device.advertisement_headers:
438438
ssdp_device.advertisement_headers[notification_type].replace(headers)
439439
else:

async_upnp_client/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ def as_lower_dict(self) -> Dict[str, Any]:
4141
"""Return the underlying dict in lowercase."""
4242
return {k.lower(): v for k, v in self._data.items()}
4343

44-
def get_lower(self, lower_key: str) -> Any:
44+
def get_lower(self, lower_key: str, default: Any = None) -> Any:
4545
"""Get a lower case key."""
4646
data_key = self._case_map.get(lower_key, _SENTINEL)
4747
if data_key is not _SENTINEL:
48-
return self._data[data_key]
49-
return None
48+
return self._data.get(data_key, default)
49+
return default
5050

5151
def replace(self, new_data: abcMapping) -> None:
5252
"""Replace the underlying dict."""

0 commit comments

Comments
 (0)