Fix background-mode plot never rendering after a zero-sized layout; rework the render loop - #136
Merged
Merged
Conversation
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #136 +/- ##
============================================
+ Coverage 69.07% 70.98% +1.91%
- Complexity 1227 1242 +15
============================================
Files 109 109
Lines 5158 5191 +33
Branches 536 543 +7
============================================
+ Hits 3563 3685 +122
+ Misses 1253 1151 -102
- Partials 342 355 +13 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
) When a background-mode plot's first layout gives it a zero-sized dimension, BufferedCanvas has no buffers, so the render thread's first pass gets a null canvas. renderOnCanvas returned early without marking the thread idle, so redraw() never notified it and the plot stayed blank forever, even after being laid out at a real size. - Mark the render thread idle on a null canvas (the reporter's fix). - Re-render on resize when the thread is already running: resize replaces the buffers with blank ones, so without this a plot that doesn't call redraw() itself stays blank after any size change. - Make isIdle volatile, since it's written on the render thread and read unsynchronized on the UI thread. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replace the isIdle flag with a redrawRequested flag guarded by renderSync. redraw() always records the request and notifies; the render thread renders, then waits until a request is pending, so requests made while it is busy drawing are honored by one further pass instead of being dropped, and a notify can no longer be lost in the window before wait(). This subsumes the earlier isIdle fix for #120. onSizeChanged no longer holds the plot lock while acquiring the buffer lock; it now takes pingPong first and applies the layout inside, the same order the render thread uses, removing a resize/render deadlock. BufferedCanvas.recycle is synchronized so onDraw cannot observe a recycled bitmap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Move the loop into a RenderThread inner class that owns its own stop and redraw-request flags. Detaching stops the current thread; attaching replaces a stopped thread immediately instead of waiting for it to exit, and an exiting thread only nulls the plot's reference and recycles the buffers if it is still the current thread. Previously a quick detach/re-attach could skip creating a new thread, or let the old thread recycle the buffers its replacement had just allocated, leaving the plot blank until its next resize. onSizeChanged starts the thread based on Thread.State.NEW rather than !isAlive(), which would have thrown for a finished thread. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
halfhp
force-pushed
the
fix-120-zero-size-background-render
branch
from
September 7, 2026 04:20
ea9db7a to
6fc0c96
Compare
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.
Fixes #120
Cause
Exactly as @harbulot diagnosed. When a background-mode plot's first layout pass gives it a zero-sized dimension,
BufferedCanvas.resizeleaves the buffers null, the render thread's first pass gets a null canvas, andrenderOnCanvasreturned early without marking the thread idle.redraw()only notified an idle thread, so it could never be woken again and the plot stayed blank even after it was laid out at a real size.Fix
Rather than patching the idle flag, the background render loop is reworked so this class of bug can't recur. Four changes in
Plot:redraw()now always records a request and notifies. The render thread renders, then waits only while no request is pending. A request made while the thread is busy drawing is honored by one further pass instead of being dropped, a notify can no longer be lost in the window beforewait(), and the null-canvas early return no longer matters because nothing depends on the thread flagging itself idle. Several requests during one render coalesce into a single extra pass.onSizeChangedonly ever started the thread; when it was already running, resize swapped in blank buffers and nothing drew until the app's nextredraw(). A resize now requests a render.onSizeChangedwassynchronizedon the plot and then took the buffer lock, while the render thread takes the buffer lock and then the plot lock insiderenderOnCanvas. A resize landing mid-render could deadlock.onSizeChangednow takes the buffer lock first and applies the layout inside it, the same order as the render thread.BufferedCanvas.recycleis also synchronized soonDrawcan't observe a recycled bitmap.RenderThreadinner class that owns its own stop and request flags. Detaching stops the current thread; attaching replaces a stopped thread immediately rather than waiting for it to exit; and an exiting thread only clears the plot's reference and recycles the buffers if it is still the current thread. Previously a quick detach and re-attach (a RecyclerView scrolling) could either skip creating a new thread, or let the old thread recycle the buffers its replacement had just allocated, leaving the plot blank until its next resize.onSizeChangedalso now starts the thread based onThread.State.NEWrather than!isAlive(), which would have thrown for a finished thread.Behavior changes
Called out in the 1.5.12 release notes since they're observable by existing background-mode users:
redraw()calls issued faster than the plot can draw used to be silently dropped, which acted as a crude frame limiter. They are now coalesced, so a burst produces one extra frame with the latest data.redraw().Verification
Four new
PlotTestcases using a background-modeMockPlotsubclass that reports renders through a latch (a Mockito spy can't be used, since the render thread captures the real instance):redraw(): the reporter's scenario.redraw()calls during it, release, and expect exactly one further render.onDrawstill having a bitmap to draw afterwards.Each test was checked to fail on the code before its corresponding change: the first two on the original code, the third on the minimal idle-flag fix, the fourth on the loop rework without ownership handling. All were rerun five times to check for flakiness.
Full suite: 215 tests, 0 failures. Lint, core release AAR and demoapp debug build pass.