Skip to content

Measure used heap, not allocated heap, before capturing a screenshot - #209

Open
rjewell808 wants to merge 1 commit into
hotwired:mainfrom
rjewell808:fix-screenshot-memory-check
Open

Measure used heap, not allocated heap, before capturing a screenshot#209
rjewell808 wants to merge 1 commit into
hotwired:mainfrom
rjewell808:fix-screenshot-memory-check

Conversation

@rjewell808

Copy link
Copy Markdown

Symptoms

Destinations presented over another screen (bottom sheets, modals) render with no backdrop — the underlying screen shows as a flat background colour instead of the page it was displaying. On affected devices this happens on every such navigation, from the first one, and never recovers within the process.

Root cause

HotwireViewScreenshotHolder.copyViewToBitmap() refuses to capture when hasEnoughMemoryForScreenshot() returns false, and that check measures the wrong thing:

val used = runtime.totalMemory().toFloat()
val max = runtime.maxMemory().toFloat()
val remaining = 1f - (used / max)

return remaining > .20

totalMemory() is the size of the heap the runtime has currently claimed from the OS, not the amount of it in use — freeMemory() is the unused portion of that heap, so memory in use is totalMemory() - freeMemory(). As written, remaining measures how much room the heap has left to grow, not how much memory is available.

Once the heap has grown to maxMemory() the expression is pinned at exactly 0.0 and every screenshot is refused for the rest of the process, however much of that heap is free. The ART heap does not shrink back, so this is one-way. Devices differ in when they reach it; some report totalMemory() == maxMemory() from process start, in which case no screenshot is ever captured at all.

The capture then returns null on the silent early-return branch — no viewScreenshotFailed, since that is only logged for PixelCopy failures — and HotwireView.addScreenshot() returns early on a null bitmap, leaving the bare HotwireView behind the modal.

Evidence

Instrumented in an app on a device that reaches the fully-grown state a couple of navigations in. Each row is one navigation to a bottom sheet; the first three values are logged from the app, viewScreenshotCreated is the library's own line:

1 - total/max (current check) 1 - (total-free)/max (actual) free heap screenshot
0.750 0.852 26.2 MB captured
0.000 0.939 240.5 MB refused
0.000 0.933 238.7 MB refused

The heap grows to its maximum between the first and second navigation. The current check flips to 0.000 and stays there — while free memory goes up, from 26 MB to 240 MB, against a screenshot that needs ~6 MB. Every subsequent modal in that process loses its backdrop.

Fix

Use totalMemory() - freeMemory() for the memory in use. The comparison is extracted to an internal function so it can be tested directly.

Testing

  • Added HotwireViewScreenshotHolderTest covering the regression (heap fully grown but mostly free) and the threshold either side of 20%.
  • ./gradlew testRelease passes for core and navigation-fragments.
  • Verified against the failing scenario in a Hotwire Native app: with the fix, viewScreenshotCreated is logged on every navigation including those where the current check reports 0.000, and modals render the underlying page as their backdrop.

hasEnoughMemoryForScreenshot() compared totalMemory() against maxMemory().
totalMemory() is the size of the heap the runtime has claimed from the OS,
not the amount of it in use, so that ratio measures how much room the heap
has left to grow rather than how much memory is available.

Once the heap has grown to maxMemory() the ratio is pinned at 0 and every
screenshot is refused, however much of that heap is free. Devices vary in
when they reach that point and some report totalMemory() == maxMemory()
from process start, in which case no screenshot is ever captured for the
life of the process.

The visible effect is that destinations presenting over another screen
lose their backdrop: HotwireView.addScreenshot() returns early on a null
bitmap, leaving the bare view behind the modal instead of the page.

Use totalMemory() - freeMemory() for the memory in use, and extract the
comparison so it can be tested.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant