Skip to content

Commit 81e186d

Browse files
authored
feat: optimize construction of outgoing packets (#1118)
1 parent 1e9de3c commit 81e186d

4 files changed

Lines changed: 282 additions & 34 deletions

File tree

bench/outgoing.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"""Benchmark for DNSOutgoing."""
2+
import socket
3+
import timeit
4+
5+
from zeroconf import DNSAddress, DNSOutgoing, DNSService, DNSText, const
6+
from zeroconf._protocol.outgoing import State
7+
8+
9+
def generate_packets() -> DNSOutgoing:
10+
out = DNSOutgoing(const._FLAGS_QR_RESPONSE | const._FLAGS_AA)
11+
address = socket.inet_pton(socket.AF_INET, "192.168.208.5")
12+
13+
additionals = [
14+
{
15+
"name": "HASS Bridge ZJWH FF5137._hap._tcp.local.",
16+
"address": address,
17+
"port": 51832,
18+
"text": b"\x13md=HASS Bridge"
19+
b" ZJWH\x06pv=1.0\x14id=01:6B:30:FF:51:37\x05c#=12\x04s#=1\x04ff=0\x04"
20+
b"ci=2\x04sf=0\x0bsh=L0m/aQ==",
21+
},
22+
{
23+
"name": "HASS Bridge 3K9A C2582A._hap._tcp.local.",
24+
"address": address,
25+
"port": 51834,
26+
"text": b"\x13md=HASS Bridge"
27+
b" 3K9A\x06pv=1.0\x14id=E2:AA:5B:C2:58:2A\x05c#=12\x04s#=1\x04ff=0\x04"
28+
b"ci=2\x04sf=0\x0bsh=b2CnzQ==",
29+
},
30+
{
31+
"name": "Master Bed TV CEDB27._hap._tcp.local.",
32+
"address": address,
33+
"port": 51830,
34+
"text": b"\x10md=Master Bed"
35+
b" TV\x06pv=1.0\x14id=9E:B7:44:CE:DB:27\x05c#=18\x04s#=1\x04ff=0\x05"
36+
b"ci=31\x04sf=0\x0bsh=CVj1kw==",
37+
},
38+
{
39+
"name": "Living Room TV 921B77._hap._tcp.local.",
40+
"address": address,
41+
"port": 51833,
42+
"text": b"\x11md=Living Room"
43+
b" TV\x06pv=1.0\x14id=11:61:E7:92:1B:77\x05c#=17\x04s#=1\x04ff=0\x05"
44+
b"ci=31\x04sf=0\x0bsh=qU77SQ==",
45+
},
46+
{
47+
"name": "HASS Bridge ZC8X FF413D._hap._tcp.local.",
48+
"address": address,
49+
"port": 51829,
50+
"text": b"\x13md=HASS Bridge"
51+
b" ZC8X\x06pv=1.0\x14id=96:14:45:FF:41:3D\x05c#=12\x04s#=1\x04ff=0\x04"
52+
b"ci=2\x04sf=0\x0bsh=b0QZlg==",
53+
},
54+
{
55+
"name": "HASS Bridge WLTF 4BE61F._hap._tcp.local.",
56+
"address": address,
57+
"port": 51837,
58+
"text": b"\x13md=HASS Bridge"
59+
b" WLTF\x06pv=1.0\x14id=E0:E7:98:4B:E6:1F\x04c#=2\x04s#=1\x04ff=0\x04"
60+
b"ci=2\x04sf=0\x0bsh=ahAISA==",
61+
},
62+
{
63+
"name": "FrontdoorCamera 8941D1._hap._tcp.local.",
64+
"address": address,
65+
"port": 54898,
66+
"text": b"\x12md=FrontdoorCamera\x06pv=1.0\x14id=9F:B7:DC:89:41:D1\x04c#=2\x04"
67+
b"s#=1\x04ff=0\x04ci=2\x04sf=0\x0bsh=0+MXmA==",
68+
},
69+
{
70+
"name": "HASS Bridge W9DN 5B5CC5._hap._tcp.local.",
71+
"address": address,
72+
"port": 51836,
73+
"text": b"\x13md=HASS Bridge"
74+
b" W9DN\x06pv=1.0\x14id=11:8E:DB:5B:5C:C5\x05c#=12\x04s#=1\x04ff=0\x04"
75+
b"ci=2\x04sf=0\x0bsh=6fLM5A==",
76+
},
77+
{
78+
"name": "HASS Bridge Y9OO EFF0A7._hap._tcp.local.",
79+
"address": address,
80+
"port": 51838,
81+
"text": b"\x13md=HASS Bridge"
82+
b" Y9OO\x06pv=1.0\x14id=D3:FE:98:EF:F0:A7\x04c#=2\x04s#=1\x04ff=0\x04"
83+
b"ci=2\x04sf=0\x0bsh=u3bdfw==",
84+
},
85+
{
86+
"name": "Snooze Room TV 6B89B0._hap._tcp.local.",
87+
"address": address,
88+
"port": 51835,
89+
"text": b"\x11md=Snooze Room"
90+
b" TV\x06pv=1.0\x14id=5F:D5:70:6B:89:B0\x05c#=17\x04s#=1\x04ff=0\x05"
91+
b"ci=31\x04sf=0\x0bsh=xNTqsg==",
92+
},
93+
{
94+
"name": "AlexanderHomeAssistant 74651D._hap._tcp.local.",
95+
"address": address,
96+
"port": 54811,
97+
"text": b"\x19md=AlexanderHomeAssistant\x06pv=1.0\x14id=59:8A:0B:74:65:1D\x05"
98+
b"c#=14\x04s#=1\x04ff=0\x04ci=2\x04sf=0\x0bsh=ccZLPA==",
99+
},
100+
{
101+
"name": "HASS Bridge OS95 39C053._hap._tcp.local.",
102+
"address": address,
103+
"port": 51831,
104+
"text": b"\x13md=HASS Bridge"
105+
b" OS95\x06pv=1.0\x14id=7E:8C:E6:39:C0:53\x05c#=12\x04s#=1\x04ff=0\x04ci=2"
106+
b"\x04sf=0\x0bsh=Xfe5LQ==",
107+
},
108+
]
109+
110+
out.add_answer_at_time(
111+
DNSText(
112+
"HASS Bridge W9DN 5B5CC5._hap._tcp.local.",
113+
const._TYPE_TXT,
114+
const._CLASS_IN | const._CLASS_UNIQUE,
115+
const._DNS_OTHER_TTL,
116+
b'\x13md=HASS Bridge W9DN\x06pv=1.0\x14id=11:8E:DB:5B:5C:C5\x05c#=12\x04s#=1'
117+
b'\x04ff=0\x04ci=2\x04sf=0\x0bsh=6fLM5A==',
118+
),
119+
0,
120+
)
121+
122+
for record in additionals:
123+
out.add_additional_answer(
124+
DNSService(
125+
record["name"], # type: ignore
126+
const._TYPE_SRV,
127+
const._CLASS_IN | const._CLASS_UNIQUE,
128+
const._DNS_HOST_TTL,
129+
0,
130+
0,
131+
record["port"], # type: ignore
132+
record["name"], # type: ignore
133+
)
134+
)
135+
out.add_additional_answer(
136+
DNSText(
137+
record["name"], # type: ignore
138+
const._TYPE_TXT,
139+
const._CLASS_IN | const._CLASS_UNIQUE,
140+
const._DNS_OTHER_TTL,
141+
record["text"], # type: ignore
142+
)
143+
)
144+
out.add_additional_answer(
145+
DNSAddress(
146+
record["name"], # type: ignore
147+
const._TYPE_A,
148+
const._CLASS_IN | const._CLASS_UNIQUE,
149+
const._DNS_HOST_TTL,
150+
record["address"], # type: ignore
151+
)
152+
)
153+
154+
return out
155+
156+
157+
out = generate_packets()
158+
159+
160+
def make_outgoing_message() -> None:
161+
out.state = State.init
162+
out.finished = False
163+
out.packets()
164+
165+
166+
count = 100000
167+
time = timeit.Timer(make_outgoing_message).timeit(count)
168+
print(f"Construction {count} outgoing messages took {time} seconds")
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
2+
import cython
3+
4+
from .incoming cimport DNSIncoming
5+
6+
7+
cdef cython.uint _CLASS_UNIQUE
8+
cdef cython.uint _DNS_PACKET_HEADER_LEN
9+
cdef cython.uint _FLAGS_QR_MASK
10+
cdef cython.uint _FLAGS_QR_QUERY
11+
cdef cython.uint _FLAGS_QR_RESPONSE
12+
cdef cython.uint _FLAGS_TC
13+
cdef cython.uint _MAX_MSG_ABSOLUTE
14+
cdef cython.uint _MAX_MSG_TYPICAL
15+
16+
17+
cdef class DNSOutgoing:
18+
19+
cdef public unsigned int flags
20+
cdef public object finished
21+
cdef public object id
22+
cdef public bint multicast
23+
cdef public cython.list packets_data
24+
cdef public object names
25+
cdef public cython.list data
26+
cdef public unsigned int size
27+
cdef public object allow_long
28+
cdef public object state
29+
cdef public cython.list questions
30+
cdef public cython.list answers
31+
cdef public cython.list authorities
32+
cdef public cython.list additionals
33+
34+
cdef _reset_for_next_packet(self)
35+
36+
cdef _write_byte(self, object value)
37+
38+
cdef _insert_short_at_start(self, object value)
39+
40+
cdef _replace_short(self, object index, object value)
41+
42+
cdef _write_int(self, object value)
43+
44+
cdef _write_question(self, object question)
45+
46+
cdef _write_record_class(self, object record)
47+
48+
cdef _check_data_limit_or_rollback(self, object start_data_length, object start_size)
49+
50+
cdef _write_questions_from_offset(self, object questions_offset)
51+
52+
cdef _write_answers_from_offset(self, object answer_offset)
53+
54+
cdef _write_records_from_offset(self, object records, object offset)
55+
56+
cdef _has_more_to_add(self, object questions_offset, object answer_offset, object authority_offset, object additional_offset)
57+
58+
@cython.locals(
59+
questions_offset=cython.uint,
60+
answer_offset=cython.uint,
61+
authority_offset=cython.uint,
62+
additional_offset=cython.uint,
63+
questions_written=cython.uint,
64+
answers_written=cython.uint,
65+
authorities_written=cython.uint,
66+
additionals_written=cython.uint,
67+
)
68+
cdef _packets(self)

src/zeroconf/_protocol/outgoing.py

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"""
2222

2323
import enum
24+
import logging
2425
from typing import Dict, List, Optional, Sequence, Tuple, Union
2526

2627
from .._cache import DNSCache
@@ -40,6 +41,11 @@
4041
from .incoming import DNSIncoming
4142

4243

44+
class State(enum.Enum):
45+
init = 0
46+
finished = 1
47+
48+
4349
class DNSOutgoing:
4450

4551
"""Object representation of an outgoing packet"""
@@ -74,7 +80,7 @@ def __init__(self, flags: int, multicast: bool = True, id_: int = 0) -> None:
7480
self.size: int = _DNS_PACKET_HEADER_LEN
7581
self.allow_long: bool = True
7682

77-
self.state = self.State.init
83+
self.state = State.init
7884

7985
self.questions: List[DNSQuestion] = []
8086
self.answers: List[Tuple[DNSRecord, float]] = []
@@ -107,10 +113,6 @@ def __repr__(self) -> str:
107113
]
108114
)
109115

110-
class State(enum.Enum):
111-
init = 0
112-
finished = 1
113-
114116
def add_question(self, record: DNSQuestion) -> None:
115117
"""Adds a question"""
116118
self.questions.append(record)
@@ -373,8 +375,10 @@ def packets(self) -> List[bytes]:
373375
will be written out to a single oversized packet no more than
374376
_MAX_MSG_ABSOLUTE in length (and hence will be subject to IP
375377
fragmentation potentially)."""
378+
return self._packets()
376379

377-
if self.state == self.State.finished:
380+
def _packets(self) -> List[bytes]:
381+
if self.state == State.finished:
378382
return self.packets_data
379383

380384
questions_offset = 0
@@ -383,25 +387,27 @@ def packets(self) -> List[bytes]:
383387
additional_offset = 0
384388
# we have to at least write out the question
385389
first_time = True
390+
debug_enable = log.isEnabledFor(logging.DEBUG)
386391

387392
while first_time or self._has_more_to_add(
388393
questions_offset, answer_offset, authority_offset, additional_offset
389394
):
390395
first_time = False
391-
log.debug(
392-
"offsets = questions=%d, answers=%d, authorities=%d, additionals=%d",
393-
questions_offset,
394-
answer_offset,
395-
authority_offset,
396-
additional_offset,
397-
)
398-
log.debug(
399-
"lengths = questions=%d, answers=%d, authorities=%d, additionals=%d",
400-
len(self.questions),
401-
len(self.answers),
402-
len(self.authorities),
403-
len(self.additionals),
404-
)
396+
if debug_enable:
397+
log.debug(
398+
"offsets = questions=%d, answers=%d, authorities=%d, additionals=%d",
399+
questions_offset,
400+
answer_offset,
401+
authority_offset,
402+
additional_offset,
403+
)
404+
log.debug(
405+
"lengths = questions=%d, answers=%d, authorities=%d, additionals=%d",
406+
len(self.questions),
407+
len(self.answers),
408+
len(self.authorities),
409+
len(self.additionals),
410+
)
405411

406412
questions_written = self._write_questions_from_offset(questions_offset)
407413
answers_written = self._write_answers_from_offset(answer_offset)
@@ -417,13 +423,14 @@ def packets(self) -> List[bytes]:
417423
answer_offset += answers_written
418424
authority_offset += authorities_written
419425
additional_offset += additionals_written
420-
log.debug(
421-
"now offsets = questions=%d, answers=%d, authorities=%d, additionals=%d",
422-
questions_offset,
423-
answer_offset,
424-
authority_offset,
425-
additional_offset,
426-
)
426+
if debug_enable:
427+
log.debug(
428+
"now offsets = questions=%d, answers=%d, authorities=%d, additionals=%d",
429+
questions_offset,
430+
answer_offset,
431+
authority_offset,
432+
additional_offset,
433+
)
427434

428435
if self.is_query() and self._has_more_to_add(
429436
questions_offset, answer_offset, authority_offset, additional_offset
@@ -447,5 +454,5 @@ def packets(self) -> List[bytes]:
447454
) > 0:
448455
log.warning("packets() made no progress adding records; returning")
449456
break
450-
self.state = self.State.finished
457+
self.state = State.finished
451458
return self.packets_data

tests/test_core.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -714,12 +714,17 @@ def test_guard_against_oversized_packets():
714714
0,
715715
)
716716

717-
# We are patching to generate an oversized packet
718-
with patch.object(outgoing, "_MAX_MSG_ABSOLUTE", 100000), patch.object(
719-
outgoing, "_MAX_MSG_TYPICAL", 100000
720-
):
721-
over_sized_packet = generated.packets()[0]
722-
assert len(over_sized_packet) > const._MAX_MSG_ABSOLUTE
717+
try:
718+
# We are patching to generate an oversized packet
719+
with patch.object(outgoing, "_MAX_MSG_ABSOLUTE", 100000), patch.object(
720+
outgoing, "_MAX_MSG_TYPICAL", 100000
721+
):
722+
over_sized_packet = generated.packets()[0]
723+
assert len(over_sized_packet) > const._MAX_MSG_ABSOLUTE
724+
except AttributeError:
725+
# cannot patch with cython
726+
zc.close()
727+
return
723728

724729
generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
725730
okpacket_record = r.DNSText(

0 commit comments

Comments
 (0)