perf(map): build the overlay frame from the accumulated strip - #9
Merged
Conversation
render_overlay allocated a fresh supersampled RGBA canvas every frame and composited the accumulated vacuumed strip onto it with the in-place Image.alpha_composite() method. On a 200x271 map at scale 4 that canvas is 1600x2168 (3.5 Mpx), and the in-place method is implemented as crop + composite + paste, so a single strip composite made three full passes over it plus two allocations. Nothing sits under the strip unless a cleaned-area tint or a target zone is being drawn, and compositing over a fully transparent canvas is the identity (extend_swath_layer leaves uncovered pixels at exactly (0,0,0,0)). So the usual frame now starts from a copy of the strip, and the built-up path uses the module-level alpha_composite(), which runs the same C routine in one pass. Output is unchanged in every branch. Measured on the target host (HAOS x86_64, 4 cores, Pillow 12.2, live map and live accumulators, 30 back-to-back render pairs): 548 ms -> 509 ms median, 44 ms saved per frame, decoded-pixel hashes identical.
Map render: start the overlay frame from a copy of the accumulated vacuumed strip instead of allocating a canvas and compositing the strip onto it, and use the module-level Image.alpha_composite in the zone branch instead of the in-place method (Pillow implements it as crop + composite + paste, three passes over a 13.9 MB buffer). Output is byte-identical, verified pixel-for-pixel on real map data. About 44 ms less per rendered frame on a Home Assistant host, roughly -8%.
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two narrow changes in
render_overlay, no behaviour change.(0, 0, 0, 0), so compositing it onto a transparent canvas was a no-op — the copy skips both the allocation and a full-canvas composite of the supersampled layer, the two most expensive fixed costs of a frame. The invariant is documented atextend_swath_layerand pinned by tests.Image.alpha_compositeinstead of the in-place method. Pillow implements the in-place variant as crop + composite + paste, i.e. three full passes over a 13.9 MB buffer.The fast path applies only when there is no cleaned-area tint and no zones, which is the usual frame; otherwise the original path runs unchanged.
Verification
master(brittleasyncio.get_event_loop()under CPython 3.14). Ruff clean.Scope
This shaves a fixed cost off every frame; it does not eliminate render-related event-loop stalls. A
ProcessPoolExecutorwas considered and rejected: the renderer keeps ~31 MB of persistent PIL layers between frames, so it would need a dedicated long-lived process with its own lifecycle and crash recovery, which is disproportionate here.