Skip to content

Commit a49913c

Browse files
committed
perf(map): eliminate redundant PNG serialization in Q10 rendering pipeline
1 parent 7613e90 commit a49913c

3 files changed

Lines changed: 48 additions & 12 deletions

File tree

‎roborock/map/b01_q10_map_parser.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -801,8 +801,8 @@ def parse_packet(self, packet: Q10MapPacket) -> ParsedMapData:
801801
"""
802802
return self.parsed_from_packet(packet)
803803

804-
def parsed_from_packet(self, packet: Q10MapPacket) -> ParsedMapData:
805-
"""Render a (possibly erase-modified) packet into a PNG + ``MapData``."""
804+
def _map_data_from_packet(self, packet: Q10MapPacket) -> MapData:
805+
"""Build MapData with a rendered base image, without PNG serialization."""
806806
image = self._render(packet)
807807

808808
map_data = MapData()
@@ -830,8 +830,14 @@ def parsed_from_packet(self, packet: Q10MapPacket) -> ParsedMapData:
830830
if packet.carpet_mask is not None:
831831
map_data.carpet_map = {i for i, value in enumerate(packet.carpet_mask) if value}
832832

833+
return map_data
834+
835+
def parsed_from_packet(self, packet: Q10MapPacket) -> ParsedMapData:
836+
"""Render a (possibly erase-modified) packet into a PNG + ``MapData``."""
837+
map_data = self._map_data_from_packet(packet)
833838
image_bytes = io.BytesIO()
834-
image.save(image_bytes, format=_MAP_FILE_FORMAT)
839+
if map_data.image is not None:
840+
map_data.image.data.save(image_bytes, format=_MAP_FILE_FORMAT)
835841
return ParsedMapData(image_content=image_bytes.getvalue(), map_data=map_data)
836842

837843
def _render(self, packet: Q10MapPacket) -> Image.Image:

‎roborock/map/b01_q10_render.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,9 @@ def render_q10_map(
109109
# areas disappear (as the app shows).
110110
render_packet = erased_packet(packet, cells)
111111

112-
parsed = parser.parsed_from_packet(render_packet)
113-
if parsed.image_content is None or parsed.map_data is None:
112+
map_data = parser._map_data_from_packet(render_packet)
113+
if map_data.image is None:
114114
raise RoborockException("Failed to render Q10 map image")
115-
map_data = parsed.map_data
116115

117116
has_drawables = False
118117
if trace_calibration is not None and trace is not None:
@@ -128,7 +127,9 @@ def render_q10_map(
128127
if has_drawables:
129128
return _draw_map_content(map_data, config=config)
130129

131-
return parsed.image_content
130+
buffer = io.BytesIO()
131+
map_data.image.data.save(buffer, format="PNG")
132+
return buffer.getvalue()
132133

133134

134135
def solve_q10_calibration(

‎tests/map/test_benchmarks.py‎

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
Q10MapPacketKind,
3838
Q10Room,
3939
parse_map_packet,
40+
parse_trace_packet,
4041
)
42+
from roborock.map.b01_q10_render import Q10MapOverlays, render_q10_map
4143
from roborock.map.map_parser import MapParser, MapParserConfig, ParsedMapData
4244

4345
if TYPE_CHECKING:
@@ -63,6 +65,7 @@ def _runner(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
6365
_V1_S5_MAP = _TESTDATA_DIR / "v1" / "s5_fw2008_with_segments.bin"
6466
_V1_S6_MAP = _TESTDATA_DIR / "v1" / "s6_fw2652_with_active_segment_and_no_mop_zone.bin"
6567
_Q10_MAP = _TESTDATA_DIR / "b01_q10_map.bin"
68+
_Q10_TRACE = _TESTDATA_DIR / "b01_q10_trace_session.bin"
6669

6770

6871
def _build_synthetic_q10_grid(width: int = 200, height: int = 200) -> Q10MapPacket:
@@ -98,14 +101,15 @@ def _build_synthetic_q10_grid(width: int = 200, height: int = 200) -> Q10MapPack
98101

99102

100103
def _validate_image(
101-
result: ParsedMapData | None,
104+
result: ParsedMapData | bytes | None,
102105
expected_size: tuple[int, int] | None = None,
103106
) -> None:
104107
"""Validate rendered PNG image integrity and pixel bounds outside the timed benchmark."""
105108
assert result is not None
106-
assert result.image_content is not None
107-
assert len(result.image_content) > 0
108-
image = Image.open(io.BytesIO(result.image_content))
109+
image_bytes = result if isinstance(result, bytes) else result.image_content
110+
assert image_bytes is not None
111+
assert len(image_bytes) > 0
112+
image = Image.open(io.BytesIO(image_bytes))
109113
assert image.format == "PNG"
110114
assert image.mode == "RGBA"
111115
assert image.getbbox() is not None # Image has non-empty drawn content
@@ -203,6 +207,17 @@ def test_benchmark_q10_map_full_scale_render_scale_4(benchmark: BenchmarkFixture
203207
_validate_image(result, (800, 800))
204208

205209

210+
def test_benchmark_q10_map_render_with_trace(benchmark: BenchmarkFixture) -> None:
211+
"""Benchmark Q10 render_q10_map end-to-end composite pipeline with trace overlay."""
212+
map_bytes = _Q10_MAP.read_bytes()
213+
trace_bytes = _Q10_TRACE.read_bytes()
214+
packet = parse_map_packet(map_bytes)
215+
trace = parse_trace_packet(trace_bytes)
216+
config = B01Q10MapParserConfig(map_scale=4)
217+
result = benchmark(render_q10_map, packet, trace=trace, overlays=Q10MapOverlays(), config=config)
218+
_validate_image(result, (32, 24))
219+
220+
206221
# ---------------------------------------------------------------------------
207222
# Standalone CLI Benchmark & Profiler
208223
# ---------------------------------------------------------------------------
@@ -225,6 +240,11 @@ def _run_benchmarks(iterations: int, warmup: int, profile: bool) -> None:
225240
p_q10_scale1 = B01Q10MapParser(B01Q10MapParserConfig(map_scale=1))
226241
p_q10_scale4 = B01Q10MapParser(B01Q10MapParserConfig(map_scale=4))
227242

243+
q10_trace_data = _Q10_TRACE.read_bytes()
244+
q10_map_packet = parse_map_packet(q10_data)
245+
q10_trace_packet = parse_trace_packet(q10_trace_data)
246+
q10_render_config = B01Q10MapParserConfig(map_scale=4)
247+
228248
benchmarks: list[tuple[str, Callable[[], Any]]] = [
229249
("V1 S5 Map (Scale 4, Default)", lambda: p_v1_s5.parse(v1_s5_data)),
230250
("V1 S6 Map (Scale 4, Default)", lambda: p_v1_s6_scale4.parse(v1_s6_data)),
@@ -236,6 +256,15 @@ def _run_benchmarks(iterations: int, warmup: int, profile: bool) -> None:
236256
("Q10 Small Map (Scale 1)", lambda: p_q10_small.parse(q10_data)),
237257
("Q10 200x200 Map (Scale 1)", lambda: p_q10_scale1.parse_packet(q10_packet_200)),
238258
("Q10 200x200 Map (Scale 4)", lambda: p_q10_scale4.parse_packet(q10_packet_200)),
259+
(
260+
"Q10 Render with Trace",
261+
lambda: render_q10_map(
262+
q10_map_packet,
263+
trace=q10_trace_packet,
264+
overlays=Q10MapOverlays(),
265+
config=q10_render_config,
266+
),
267+
),
239268
]
240269

241270
print(f"\nRunning {len(benchmarks)} benchmarks ({warmup} warmup, {iterations} timed iterations)...\n")
@@ -255,7 +284,7 @@ def _run_benchmarks(iterations: int, warmup: int, profile: bool) -> None:
255284
min_ms = timings[0]
256285
mean_ms = sum(timings) / len(timings)
257286
med_ms = timings[len(timings) // 2]
258-
p95_ms = timings[int(len(timings) * 0.95)]
287+
p95_ms = timings[max(0, int((len(timings) - 1) * 0.95))]
259288
ops_per_sec = 1000.0 / mean_ms if mean_ms > 0 else float("inf")
260289

261290
results.append((name, min_ms, med_ms, mean_ms, p95_ms, ops_per_sec))

0 commit comments

Comments
 (0)