11"""Benchmarks for Roborock V1 and Q10 map parsing and rendering.
22
3+ Includes permutations for:
4+ - Different scaling values (scale 1, 2, 4)
5+ - Drawable overlay variations (none, default, all)
6+ - Real V1 S5 and S6 map payloads
7+ - Q10 wire unpack and multi-room composite rendering
8+ - Pixel output validation (dimensions, format, bounding box)
9+
310Can be run via pytest:
411 uv run pytest tests/map/test_benchmarks.py
512 uv run pytest tests/map/test_benchmarks.py --codspeed
1926from typing import TYPE_CHECKING , Any
2027
2128import pytest
29+ from PIL import Image
30+ from vacuum_map_parser_base .config .drawable import Drawable
2231
2332from roborock .map .b01_q10_map_parser import (
2433 B01Q10MapParser ,
34+ B01Q10MapParserConfig ,
2535 Q10MapPacket ,
2636 Q10MapPacketKind ,
2737 Q10Room ,
2838 parse_map_packet ,
2939)
30- from roborock .map .map_parser import MapParser , MapParserConfig
40+ from roborock .map .map_parser import MapParser , MapParserConfig , ParsedMapData
3141
3242if TYPE_CHECKING :
33- from pytest_codspeed import BenchmarkFixture
43+ from pytest_benchmark . fixture import BenchmarkFixture
3444else :
3545 try :
36- from pytest_codspeed import BenchmarkFixture
46+ from pytest_benchmark . fixture import BenchmarkFixture
3747 except ImportError :
38- BenchmarkFixture = Any
48+ try :
49+ from pytest_codspeed import BenchmarkFixture
50+ except ImportError :
51+ BenchmarkFixture = Any
3952
40- @pytest .fixture
41- def benchmark () -> Callable [..., Any ]:
42- def _runner (func : Callable [..., Any ], * args : Any , ** kwargs : Any ) -> Any :
43- return func (* args , ** kwargs )
53+ @pytest .fixture
54+ def benchmark () -> Callable [..., Any ]:
55+ def _runner (func : Callable [..., Any ], * args : Any , ** kwargs : Any ) -> Any :
56+ return func (* args , ** kwargs )
4457
45- return _runner
58+ return _runner
4659
4760
4861_TESTDATA_DIR = Path (__file__ ).resolve ().parent / "testdata"
@@ -83,22 +96,78 @@ def _build_synthetic_q10_grid(width: int = 200, height: int = 200) -> Q10MapPack
8396 )
8497
8598
99+ def _validate_image (
100+ result : ParsedMapData | None ,
101+ expected_size : tuple [int , int ] | None = None ,
102+ ) -> None :
103+ """Validate rendered PNG image integrity and pixel bounds outside the timed benchmark."""
104+ assert result is not None
105+ assert result .image_content is not None
106+ assert len (result .image_content ) > 0
107+ image = Image .open (io .BytesIO (result .image_content ))
108+ assert image .format == "PNG"
109+ assert image .mode == "RGBA"
110+ assert image .getbbox () is not None # Image has non-empty drawn content
111+ if expected_size is not None :
112+ assert image .size == expected_size
113+
114+
115+ # ---------------------------------------------------------------------------
116+ # V1 Map Benchmarks
117+ # ---------------------------------------------------------------------------
118+
119+
86120def test_benchmark_v1_s5_map_parse (benchmark : BenchmarkFixture ) -> None :
87- """Benchmark parsing a real Roborock V1 S5 map with 2 rooms."""
121+ """Benchmark parsing a real Roborock V1 S5 map with 2 rooms (default scale 4) ."""
88122 raw_data = _V1_S5_MAP .read_bytes ()
89123 parser = MapParser (MapParserConfig ())
90124 result = benchmark (parser .parse , raw_data )
91- assert result is not None
92- assert result .image_content is not None
125+ _validate_image (result , (1024 , 964 ))
93126
94127
95- def test_benchmark_v1_s6_map_parse (benchmark : BenchmarkFixture ) -> None :
96- """Benchmark parsing a real Roborock V1 S6 map with 6 rooms and active segment ."""
128+ def test_benchmark_v1_s6_map_scale_4_default (benchmark : BenchmarkFixture ) -> None :
129+ """Benchmark parsing a real Roborock V1 S6 map at default scale 4 (1644x1344 px) ."""
97130 raw_data = _V1_S6_MAP .read_bytes ()
98- parser = MapParser (MapParserConfig ())
131+ parser = MapParser (MapParserConfig (map_scale = 4 ))
99132 result = benchmark (parser .parse , raw_data )
100- assert result is not None
101- assert result .image_content is not None
133+ _validate_image (result , (1644 , 1344 ))
134+
135+
136+ def test_benchmark_v1_s6_map_scale_2 (benchmark : BenchmarkFixture ) -> None :
137+ """Benchmark parsing a real Roborock V1 S6 map at scale 2 (822x672 px)."""
138+ raw_data = _V1_S6_MAP .read_bytes ()
139+ parser = MapParser (MapParserConfig (map_scale = 2 ))
140+ result = benchmark (parser .parse , raw_data )
141+ _validate_image (result , (822 , 672 ))
142+
143+
144+ def test_benchmark_v1_s6_map_scale_1 (benchmark : BenchmarkFixture ) -> None :
145+ """Benchmark parsing a real Roborock V1 S6 map at scale 1 (411x336 px)."""
146+ raw_data = _V1_S6_MAP .read_bytes ()
147+ parser = MapParser (MapParserConfig (map_scale = 1 ))
148+ result = benchmark (parser .parse , raw_data )
149+ _validate_image (result , (411 , 336 ))
150+
151+
152+ def test_benchmark_v1_s6_map_no_drawables (benchmark : BenchmarkFixture ) -> None :
153+ """Benchmark parsing a real Roborock V1 S6 map with all drawables disabled."""
154+ raw_data = _V1_S6_MAP .read_bytes ()
155+ parser = MapParser (MapParserConfig (map_scale = 4 , drawables = []))
156+ result = benchmark (parser .parse , raw_data )
157+ _validate_image (result , (1644 , 1344 ))
158+
159+
160+ def test_benchmark_v1_s6_map_all_drawables (benchmark : BenchmarkFixture ) -> None :
161+ """Benchmark parsing a real Roborock V1 S6 map with all available drawables enabled."""
162+ raw_data = _V1_S6_MAP .read_bytes ()
163+ parser = MapParser (MapParserConfig (map_scale = 4 , drawables = list (Drawable )))
164+ result = benchmark (parser .parse , raw_data )
165+ _validate_image (result , (1644 , 1344 ))
166+
167+
168+ # ---------------------------------------------------------------------------
169+ # Q10 Map Benchmarks
170+ # ---------------------------------------------------------------------------
102171
103172
104173def test_benchmark_q10_map_packet_unpack (benchmark : BenchmarkFixture ) -> None :
@@ -114,15 +183,23 @@ def test_benchmark_q10_map_parse_and_render(benchmark: BenchmarkFixture) -> None
114183 raw_data = _Q10_MAP .read_bytes ()
115184 parser = B01Q10MapParser ()
116185 result = benchmark (parser .parse , raw_data )
117- assert result . image_content is not None
186+ _validate_image ( result , ( 32 , 24 ))
118187
119188
120- def test_benchmark_q10_map_full_scale_render (benchmark : BenchmarkFixture ) -> None :
121- """Benchmark Q10 composite rendering on a realistic 200x200 4-room floorplan."""
189+ def test_benchmark_q10_map_full_scale_render_scale_1 (benchmark : BenchmarkFixture ) -> None :
190+ """Benchmark Q10 composite rendering on a realistic 200x200 4-room floorplan at scale 1 ."""
122191 packet = _build_synthetic_q10_grid (200 , 200 )
123- parser = B01Q10MapParser ()
192+ parser = B01Q10MapParser (B01Q10MapParserConfig ( map_scale = 1 ) )
124193 result = benchmark (parser .parse_packet , packet )
125- assert result .image_content is not None
194+ _validate_image (result , (200 , 200 ))
195+
196+
197+ def test_benchmark_q10_map_full_scale_render_scale_4 (benchmark : BenchmarkFixture ) -> None :
198+ """Benchmark Q10 composite rendering on a realistic 200x200 4-room floorplan at scale 4."""
199+ packet = _build_synthetic_q10_grid (200 , 200 )
200+ parser = B01Q10MapParser (B01Q10MapParserConfig (map_scale = 4 ))
201+ result = benchmark (parser .parse_packet , packet )
202+ _validate_image (result , (800 , 800 ))
126203
127204
128205# ---------------------------------------------------------------------------
@@ -136,15 +213,28 @@ def _run_benchmarks(iterations: int, warmup: int, profile: bool) -> None:
136213 q10_data = _Q10_MAP .read_bytes ()
137214 q10_packet_200 = _build_synthetic_q10_grid (200 , 200 )
138215
139- v1_parser = MapParser (MapParserConfig ())
140- q10_parser = B01Q10MapParser ()
216+ p_v1_s5 = MapParser (MapParserConfig ())
217+ p_v1_s6_scale4 = MapParser (MapParserConfig (map_scale = 4 ))
218+ p_v1_s6_scale2 = MapParser (MapParserConfig (map_scale = 2 ))
219+ p_v1_s6_scale1 = MapParser (MapParserConfig (map_scale = 1 ))
220+ p_v1_s6_no_draw = MapParser (MapParserConfig (map_scale = 4 , drawables = []))
221+ p_v1_s6_all_draw = MapParser (MapParserConfig (map_scale = 4 , drawables = list (Drawable )))
222+
223+ p_q10_small = B01Q10MapParser ()
224+ p_q10_scale1 = B01Q10MapParser (B01Q10MapParserConfig (map_scale = 1 ))
225+ p_q10_scale4 = B01Q10MapParser (B01Q10MapParserConfig (map_scale = 4 ))
141226
142227 benchmarks : list [tuple [str , Callable [[], Any ]]] = [
143- ("V1 S5 Map (Parse & Render)" , lambda : v1_parser .parse (v1_s5_data )),
144- ("V1 S6 Map (Parse & Render)" , lambda : v1_parser .parse (v1_s6_data )),
228+ ("V1 S5 Map (Scale 4, Default)" , lambda : p_v1_s5 .parse (v1_s5_data )),
229+ ("V1 S6 Map (Scale 4, Default)" , lambda : p_v1_s6_scale4 .parse (v1_s6_data )),
230+ ("V1 S6 Map (Scale 2)" , lambda : p_v1_s6_scale2 .parse (v1_s6_data )),
231+ ("V1 S6 Map (Scale 1)" , lambda : p_v1_s6_scale1 .parse (v1_s6_data )),
232+ ("V1 S6 Map (No Drawables)" , lambda : p_v1_s6_no_draw .parse (v1_s6_data )),
233+ ("V1 S6 Map (All Drawables)" , lambda : p_v1_s6_all_draw .parse (v1_s6_data )),
145234 ("Q10 Wire Packet (Unpack only)" , lambda : parse_map_packet (q10_data )),
146- ("Q10 Map (Parse & Render)" , lambda : q10_parser .parse (q10_data )),
147- ("Q10 200x200 Map (Composite Render)" , lambda : q10_parser .parse_packet (q10_packet_200 )),
235+ ("Q10 Small Map (Scale 1)" , lambda : p_q10_small .parse (q10_data )),
236+ ("Q10 200x200 Map (Scale 1)" , lambda : p_q10_scale1 .parse_packet (q10_packet_200 )),
237+ ("Q10 200x200 Map (Scale 4)" , lambda : p_q10_scale4 .parse_packet (q10_packet_200 )),
148238 ]
149239
150240 print (f"\n Running { len (benchmarks )} benchmarks ({ warmup } warmup, { iterations } timed iterations)...\n " )
0 commit comments