perf(app): copy sliced sprite frames into standalone bitmaps (~1.5GB -> ~131MB)#20
Merged
Merged
Conversation
SpriteSlicer.slice() returned CGImage.cropping(to:) views that each retained the full decoded 1536x1872 spritesheet; at runtime ImageIO cached one 11MB full-sheet decode per drawn frame (158 copies = 1.3GB). Render each frame into its own frame-sized CGContext instead, dropping app footprint from ~1.5GB to ~130MB. Fixes upstream issue ntd4996#19 locally.
Owner
|
Merged, thank you for the thorough investigation and fix. The vmmap breakdown and the standalone-bitmap approach were exactly right, and the SpriteSlicerTests are a great addition. Memory drops from ~1.5 GB to ~131 MB on my end too. This also closes #19. Added you to the contributors page (agentpet.thenightwatcher.online/contributors). 🙏 |
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.
Summary
Fixes #19 — AgentPet sat at ~1.5 GB physical footprint with a single pet displayed, because
SpriteSlicer.slice()produced frames withCGImage.cropping(to:). Cropping returns a view that references the parent image's storage: every frame retained the full decoded 1536×1872 sheet, and ImageIO cached one 11 MB full-sheet decode per drawn frame (measured 158 copies = 1.3 GB resident).This PR renders each sliced frame into its own frame-sized
CGContextinstead, so a frame owns a ~156 KB standalone backing store and the parent sheet becomes reclaimable.Measured impact
Same machine, same pet (
nika), equal ~8 min uptime, measured withvmmap:Visual behaviour is unchanged — frames are pixel-identical, only the backing store changes.
Changes
Sources/App/SpriteSlicer.swift— draw each crop into a frame-sized bitmap context (bytesPerRow = width * 4, same colorspace/alpha aspixelData()) and appendmakeImage()'s result; context/makeImagefailures skip the frame, matching the existing nil-crop behaviour. Public signature and the gutter-detection logic are untouched.Tests/AgentPetAppTests/SpriteSlicerTests.swift— new tests using a procedurally generated spritesheet (no fixture files): grid detection, per-frame pixel fidelity, the standalone-backing-store contract (bytesPerRow == width * 4, which fails against the old cropping-view implementation), and the fully-transparent-sheet edge case.swift test: 152 tests, 0 failures.🤖 Generated with Claude Code