Skip to content

ADFA-4331: Decode opened plugin tabs off the main thread#1416

Open
fryanpan wants to merge 3 commits into
stagefrom
ADFA-4331-restoreplugintabs-json-main-thread
Open

ADFA-4331: Decode opened plugin tabs off the main thread#1416
fryanpan wants to merge 3 commits into
stagefrom
ADFA-4331-restoreplugintabs-json-main-thread

Conversation

@fryanpan

@fryanpan fryanpan commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Jira Ticket: https://appdevforall.atlassian.net/browse/ADFA-4331
Sentry Issue: https://appdevforall-inc-9p.sentry.io/issues/APPDEVFORALL-1J

Question

Looks like this file is part of AndroidIDE -- do we make changes here or upstream?

Reproduction Details

A main-thread / ANR-risk performance issue (not a crash). EditorHandlerActivity.restoreOpenedPluginTabs() read the cached-tabs JSON from SharedPreferences and ran Gson().fromJson(...) synchronously on the main thread during startup, stalling the UI.

Stack Trace

No exception (profiling issue). Suspect function:

Gson.fromJson(JsonReader, TypeToken): Object   — on the main thread (warm start)

(issue type: profile_json_decode_main_thread · device.class: low)

User Steps

User steps leading up to crash, based on Sentry breadcrumbs:

  • App warm-start with previously-open plugin tabs to restore; the tab-state JSON is decoded on the main thread during onStart, blocking the first frames. (Profiling aggregate; tagged to the startup transaction.)

Was able to reproduce in a unit test?

Yes.
RestorePluginTabsThreadTest (:app, Robolectric) pins Dispatchers.Main, invokes the real private restoreOpenedPluginTabs(), and captures the thread on which its pref write executes (via the EventBus PreferenceChangeEvent). Baseline: write thread == main (FAIL); branch: write thread == DefaultDispatcher-worker (off main). Mutation-sensitive: flipping the prod file flips red/green.

What Was Fixed

Move the pref read + Gson decode + clearing write into withContext(Dispatchers.IO/Default) inside lifecycleScope.launch; UI updates stay on Main.

Testing

:app:testV8DebugUnitTest --tests …RestorePluginTabsThreadTest → green (red on baseline). Local CodeRabbit review: no findings. Reviewer notes (local): remove the leftover println("PROBE: …") debug line in the test; the restore now races the sibling onStart reopen-files coroutine (serialized on Main, theoretical only).


Fixes APPDEVFORALL-1J

@fryanpan fryanpan marked this pull request as ready for review June 19, 2026 11:15
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@fryanpan, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 27 minutes and 21 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 461c0821-a65d-4d9d-8130-bacc634b93d2

📥 Commits

Reviewing files that changed from the base of the PR and between 0852a5e and 99467d1.

📒 Files selected for processing (2)
  • app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
  • app/src/test/java/com/itsaky/androidide/activities/editor/RestorePluginTabsThreadTest.kt
📝 Walkthrough

Walkthrough

restoreOpenedPluginTabs() is refactored from a synchronous implementation to a coroutine-based one that reads SharedPreferences on Dispatchers.IO and parses JSON on Dispatchers.Default. A new Robolectric regression test (ADFA-4331) verifies that the preference-clearing write occurs off the main thread using an EventBus subscriber and CountDownLatch.

Changes

Async plugin tab restore with threading regression test

Layer / File(s) Summary
Async restore implementation and threading regression test
app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt, app/src/test/java/com/itsaky/androidide/activities/editor/RestorePluginTabsThreadTest.kt
restoreOpenedPluginTabs is rewritten to launch a coroutine reading PREF_KEY_OPEN_PLUGIN_TABS on Dispatchers.IO, parsing JSON on Dispatchers.Default, and clearing the preference key asynchronously. The regression test seeds preferences, bypasses UI selection via reflection, captures the write-thread name through an EventBus PreferenceChangeEvent subscriber with a CountDownLatch, pumps the test scheduler, and asserts the write occurred off the main thread.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • appdevforall/CodeOnTheGo#853: Originally introduced EditorHandlerActivity.restoreOpenedPluginTabs() and the PREF_KEY_OPEN_PLUGIN_TABS SharedPreferences persistence/restore logic that this PR refactors to run asynchronously.

Suggested reviewers

  • dara-abijo-adfa
  • jomen-adfa

Poem

🐇 Hoppity-hop, off the main thread I leap,
SharedPrefs and JSON no longer make UI weep.
A latch and a bus catch the write in the night,
Coroutines carry the tabs with delight.
The test sniffs the thread — background, hooray!
This bunny approves of the async display! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: moving the JSON decoding of plugin tabs off the main thread to improve performance during startup.
Description check ✅ Passed The description comprehensively relates to the changeset, detailing the performance issue, reproduction steps, testing approach, and the specific fix applied to move blocking operations off the main thread.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-4331-restoreplugintabs-json-main-thread

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

fryanpan and others added 3 commits June 19, 2026 09:58
Sentry APPDEVFORALL-1J. Move Gson decode in restoreOpenedPluginTabs to
Dispatchers.Default; keep UI updates on main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…in thread

Failing-first repro that invokes the real private restoreOpenedPluginTabs()
under Robolectric and captures the thread on which the cached-tabs preference
write executes (via the EventBus PreferenceChangeEvent posted by
PreferenceManager.putString). UI tab-selection is skipped by pre-seeding the
private pluginTabIndices map.

Pre-fix: read + Gson decode + clearing write run synchronously on the main
thread -> write thread == main thread -> test FAILS.
Fixed: read/decode/write are wrapped in withContext(Dispatchers.IO/Default) ->
write runs on a background worker -> test PASSES.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add KDoc to restoreOpenedPluginTabs() and the repro @test; remove a
leftover println("PROBE: ...") debug line in RestorePluginTabsThreadTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@fryanpan fryanpan force-pushed the ADFA-4331-restoreplugintabs-json-main-thread branch from 9d4f64c to 99467d1 Compare June 19, 2026 16:58
@hal-eisen-adfa

Copy link
Copy Markdown
Collaborator

Please be careful about possible collisions with PR #1426

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.

3 participants