Skip to content

Fix background-mode plot never rendering after a zero-sized layout; rework the render loop - #136

Merged
halfhp merged 4 commits into
masterfrom
fix-120-zero-size-background-render
Sep 7, 2026
Merged

Fix background-mode plot never rendering after a zero-sized layout; rework the render loop#136
halfhp merged 4 commits into
masterfrom
fix-120-zero-size-background-render

Conversation

@halfhp

@halfhp halfhp commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Fixes #120

Cause

Exactly as @harbulot diagnosed. When a background-mode plot's first layout pass gives it a zero-sized dimension, BufferedCanvas.resize leaves the buffers null, the render thread's first pass gets a null canvas, and renderOnCanvas returned 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:

  1. Request flag instead of idle flag. 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 before wait(), 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.
  2. Re-render on resize. onSizeChanged only ever started the thread; when it was already running, resize swapped in blank buffers and nothing drew until the app's next redraw(). A resize now requests a render.
  3. Consistent lock order. onSizeChanged was synchronized on the plot and then took the buffer lock, while the render thread takes the buffer lock and then the plot lock inside renderOnCanvas. A resize landing mid-render could deadlock. onSizeChanged now takes the buffer lock first and applies the layout inside it, the same order as the render thread. BufferedCanvas.recycle is also synchronized so onDraw can't observe a recycled bitmap.
  4. Explicit thread ownership across detach/re-attach. The loop moves into a RenderThread inner 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. onSizeChanged also now starts the thread based on Thread.State.NEW rather 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.
  • Resized plots re-render automatically instead of staying blank until the next redraw().
  • The resize/render deadlock is gone.
  • A quick detach and re-attach no longer leaves the plot without a render thread or with recycled buffers.

Verification

Four new PlotTest cases using a background-mode MockPlot subclass that reports renders through a latch (a Mockito spy can't be used, since the render thread captures the real instance):

  • Lay out at 100x0, wait for the thread to park, lay out at 100x100, and expect a render from the resize alone.
  • Same, but with an explicit redraw(): the reporter's scenario.
  • Block the first render, issue two redraw() calls during it, release, and expect exactly one further render.
  • Block a render, detach and re-attach while the old thread is stuck in it, release, and expect the replacement thread to render on start and on request, with onDraw still 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.

@codecov-commenter

codecov-commenter commented Sep 7, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 84.41558% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.98%. Comparing base (f8d00ce) to head (6fc0c96).

Files with missing lines Patch % Lines
...dplot-core/src/main/java/com/androidplot/Plot.java 84.41% 3 Missing and 9 partials ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@halfhp halfhp changed the title Fix background-mode plot never rendering after a zero-sized layout Fix background-mode plot never rendering after a zero-sized layout; rework the render loop Sep 7, 2026
halfhp and others added 4 commits September 6, 2026 23:19
)

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
halfhp force-pushed the fix-120-zero-size-background-render branch from ea9db7a to 6fc0c96 Compare September 7, 2026 04:20
@halfhp
halfhp merged commit c15129b into master Sep 7, 2026
1 check passed
@halfhp
halfhp deleted the fix-120-zero-size-background-render branch September 7, 2026 04:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plot not displayed with background thread if initial size is 0

2 participants