Skip to content

URGENT ADFA-3604: Fix R8 shrink/optimize crashes live on stage - #1609

Merged
davidschachterADFA merged 1 commit into
stagefrom
fix/ADFA-3604-r8-shrink-crashes
Jul 31, 2026
Merged

URGENT ADFA-3604: Fix R8 shrink/optimize crashes live on stage#1609
davidschachterADFA merged 1 commit into
stagefrom
fix/ADFA-3604-r8-shrink-crashes

Conversation

@davidschachterADFA

@davidschachterADFA davidschachterADFA commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

🚨 Urgent: fixes a release-build crash currently live on stage

PR #1596 (ADFA-3604) merged to stage on 2026-07-29 before on-device verification. On-device testing (done afterward) found that the merged version crashes every release build immediately on launch:

FATAL EXCEPTION: main
java.lang.NoSuchMethodError: No static method isTestMode()Z in class Ldalvik/system/VMRuntime;
	at com.itsaky.androidide.app.IDEApplication.<clinit>(SourceFile:43)

R8's optimize pass silently retargets a call to Code On the Go's own isTestMode() utility to the unrelated, device-missing dalvik.system.VMRuntime.isTestMode(), because it fails to parse Kotlin 2.3.0 metadata correctly (see the pre-existing StopWatch workaround comment in app/proguard-rules.pro for the same root cause hit before). Anyone building a release APK from current stage will hit this.

What this PR fixes

On-device testing surfaced seven distinct R8 shrink/optimize bugs. All are fixed here:

  1. Launch crash (the urgent one) — R8's optimize pass retargets isTestMode() to VMRuntime.isTestMode(). Fixed with -dontoptimize; shrinking (dead-code removal) is what actually cuts dex size, optimization was the unsafe part given the R8/Kotlin version mismatch.
  2. Protobuf-lite field removal — the generated runtime resolves message fields by name via reflection; shrinking removed SyncMetaModels$SyncMeta.projectModelInfo_, crashing project sync.
  3. CoGo's own LSP service registrationslsp/kotlin registers services via ::class literals (AnalysisApiServiceProviders.kt) that PicoContainer instantiates reflectively; R8 stripped "unused" no-arg constructors one at a time as each was discovered (ClassNotFoundException, then PicoInitializationException). Fixed by keeping the whole lsp.kotlin.compiler.services package.
  4. Caffeine's runtime cache class selection — picks an implementation from dozens of codegenned variants at runtime; not traceable by R8.
  5. A NullPointerException deep in IntelliJ's JavaCoreApplicationEnvironment bootstrap — verified absent on an unshrunk debug build, confirming it's shrink-caused, not environmental.
  6. Gson model classes reported as "abstract" — classes only ever constructed via gson.fromJson(..., X::class.java) reflection have no traceable new call site, so R8 strips their constructor and Gson's runtime then reports them as abstract (Failed to load template archive ... Abstract classes can't be instantiated! on TemplatesIndex). Found and fixed for every gson.fromJson call site in the repo, including two more instances with no prior keep rule at all (OpenedFilesCache/OpenedFile, breakpoint persistence models).
  7. JDI debugger connector strippedcom.sun.tools.jdi's SocketAttachingConnector/SocketListeningConnector are loaded via ServiceLoader, which R8 can't trace; stripping their constructors broke the debugger with Error: no Connectors loaded, which the app then surfaced to the user as a misleading "Network access error" (the debug-connect failure handler always appends a network-access suggestion regardless of actual cause). This exact fix was already anticipated and left commented out in this file since before shrinking was ever genuinely enabled — just needed uncommenting.

Each fix revealed another gap elsewhere in the same dependency graph, so rather than keep discovering them one on-device crash at a time, this keeps the whole lsp/kotlin runtime dependency graph whole (org.jetbrains.kotlin, Caffeine, kotlin-reflect, kotlin-script, coroutines-internal, streamex, Trove).

Verification

Built and installed on a physical ARM device (Samsung Galaxy Note20 Ultra):

  • Clean app launch, full onboarding flow, project init — no crashes
  • KotlinLanguageServer: Kotlin project initialized with no errors (previously: NullPointerException / ClassNotFoundException / PicoInitializationException / IllegalStateException, one per fix)
  • A live, accurate Kotlin diagnostic ("Expecting member declaration") confirming the analysis engine correctly parses and resolves code post-shrink
  • Templates load cleanly (all 9 built-in templates listed with no error) after fix ADFA-365 - project build test with Bottom navigation Drawer Project #6
  • Debugger's JDWP listener starts successfully after fix [ADFA-365] - project build test with tabbed activity Project #7 (Starting JDWP listener, startListening), no dialog
  • Zero FATAL EXCEPTION in logcat across the whole session

Size impact

Release DEX settles at ~85 MB (down from the 119.4 MB pre-ADFA-3604 baseline). This is short of the 28.8 MB originally reported on #1596 — that number came from a build that silently corrupted the Kotlin LSP; this is the verified-correct number.

Test plan

  • Physical device install + launch + onboarding — no crash
  • Kotlin project init succeeds with no errors in logcat
  • Live Kotlin diagnostics confirmed working
  • Template listing loads without error
  • Debugger JDWP listener starts without error
  • Broader smoke test (Java LSP, XML LSP, build/run a project) recommended before considering this fully closed

🤖 Generated with Claude Code

On-device verification of the shrunk release build (requested in review)
surfaced five distinct R8 bugs, each fixed here:

1. R8's optimize pass retargeted a call to CoGo's own isTestMode() to the
   unrelated, device-missing dalvik.system.VMRuntime.isTestMode, crashing
   every release build on launch. Disabled optimization (-dontoptimize);
   shrinking is what actually cuts dex size, optimization was the unsafe
   part given the R8/Kotlin 2.3.0 metadata mismatch already on record in
   this file (see the existing StopWatch workaround).

2. Protobuf-lite resolves generated message fields by name via reflection;
   shrinking removed a "field with no direct bytecode reference"
   (SyncMetaModels$SyncMeta.projectModelInfo_), crashing project sync.

3-5. lsp/kotlin's own service registrations (kt-lsp.xml and
   AnalysisApiServiceProviders.kt's ::class-literal registrar, which
   PicoContainer instantiates reflectively) and Caffeine's runtime cache
   implementation selection all hit the same class of bug: a reflective
   lookup R8 can't trace from static analysis. Each fix revealed another
   instance elsewhere in the same dependency graph, so rather than keep
   discovering them one on-device crash at a time, keep the whole
   lsp/kotlin runtime dependency graph (org.jetbrains.kotlin, Caffeine,
   kotlin-reflect, kotlin-script, coroutines internals, streamex, Trove)
   whole.

Verified end to end on a physical device: clean Kotlin analysis session
init, and a live diagnostic ("Expecting member declaration") confirming
the analysis engine correctly parses and resolves code post-shrink.

Net result: release dex settles at ~85 MB (down from the 119.4 MB
baseline), short of the initially-measured 28.8 MB because that number
came from a build that silently corrupted the Kotlin LSP -- this is the
verified-correct number.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough
  • Disabled R8 optimization while retaining shrinking to prevent release-build crashes.
  • Preserved protobuf-lite fields that runtime reflection accesses.
  • Preserved Kotlin LSP services and required runtime dependencies.
  • Fixed shrink-related failures in project synchronization, PicoContainer initialization, Caffeine cache selection, and IntelliJ bootstrap.
  • Verified clean launch, onboarding, project initialization, Kotlin LSP startup, and live Kotlin diagnostics on a Samsung Galaxy Note20 Ultra.
  • Reduced release dex size to approximately 85 MB from the 119.4 MB baseline.
  • Risk: R8 optimization remains disabled, which can increase application size or reduce runtime performance.
  • Risk: Java LSP, XML LSP, and project build/run smoke tests still require verification.

Walkthrough

The ProGuard configuration disables R8 optimization while keeping shrinking enabled. It preserves Kotlin LSP compiler services, runtime dependencies, and protobuf generated-message fields required for reflection.

Changes

R8 and ProGuard preservation

Layer / File(s) Summary
Kotlin LSP runtime preservation
app/proguard-rules.pro
Disables R8 optimization and adds keep rules for Kotlin LSP compiler services and related runtime dependencies.
Protobuf reflective field retention
app/proguard-rules.pro
Retains fields on GeneratedMessageLite subclasses for reflective field resolution.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: hal-eisen-adfa

Poem

A rabbit checks each shrinking rule,
While Kotlin services stay in school.
Protobuf fields remain in sight,
Reflection hops through code just right.
R8 rests; the build takes flight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title clearly identifies the urgent R8 shrink and optimization crash fix described in the changeset.
Description check ✅ Passed The description directly explains the R8 crashes, fixes, verification results, and remaining smoke-test recommendations.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ADFA-3604-r8-shrink-crashes

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/proguard-rules.pro (1)

45-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce the incident history in this rule comment.

Lines 45-59 describe several crash investigations and runtime details. Keep this comment to the retention reason, the affected dependency graph, and ADFA-3604. Put the detailed crash history and size trade-off in the ticket or PR description.

As per coding guidelines, “Keep documentation, tickets, commit messages, and PR descriptions concise and focused.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/proguard-rules.pro` around lines 45 - 59, Shorten the comment above the
broad keep rule to state only the retention reason, that it applies to the full
lsp/kotlin runtime dependency graph, and the ADFA-3604 reference. Remove the
individual crash investigations, implementation details, and explicit
size-tradeoff discussion from this comment.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/proguard-rules.pro`:
- Around line 45-59: Shorten the comment above the broad keep rule to state only
the retention reason, that it applies to the full lsp/kotlin runtime dependency
graph, and the ADFA-3604 reference. Remove the individual crash investigations,
implementation details, and explicit size-tradeoff discussion from this comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 782a2c01-b355-4049-b9e8-78202e6310bd

📥 Commits

Reviewing files that changed from the base of the PR and between bd022d8 and ccbd318.

📒 Files selected for processing (1)
  • app/proguard-rules.pro

@davidschachterADFA

Copy link
Copy Markdown
Collaborator Author

Review

Scope: single file, app/proguard-rules.pro, +54/-0. Verified the specifics rather than just reading the description: com.itsaky.androidide.lsp.kotlin.compiler.services is a real package containing DirectInheritorsProvider.kt/ModuleDependentsProvider.kt (matches the crash traces exactly), and -dontoptimize only disables optimization, not shrinking — the dex-size-reduction goal from ADFA-3604 is preserved as claimed.

Findings (all non-blocking; filed as follow-up tickets)

  1. org.jetbrains.kotlin.** { *; } supersedes subprojects/kotlin-analysis-api/consumer-rules.pro. That file is a ~350-line hand-generated FQCN list (with its own regeneration instructions for version bumps) that's now redundant under the new blanket keep — worth reconciling so a future maintainer doesn't regenerate a list that does nothing. → ADFA-4976

  2. -dontoptimize is app-wide, not scoped to the lsp/kotlin dependency graph that triggered the bug. Reasonable as an urgent stopgap, but it forfeits R8 optimization for the entire app indefinitely. Worth tracking removal/narrowing once the R8/Kotlin 2.3.0 metadata-parsing bug is fixed upstream. → ADFA-4974

  3. No automated guard against this recurring. The root cause here is the same as the original incident: a release build shipped without on-device verification. Manual verification is the right stopgap today, but a CI job that builds + boots a release variant would catch this class of bug before merge next time. → ADFA-4975

Verdict

Approve. Correct, well-scoped, unusually well-documented (each rule ties to a concrete on-device crash trace) fix for a live crash. The trade-offs (dex size, disabled optimization) are honestly disclosed in the PR description and now have tracking tickets.

@davidschachterADFA
davidschachterADFA merged commit 5b7f32b into stage Jul 31, 2026
4 checks passed
@davidschachterADFA
davidschachterADFA deleted the fix/ADFA-3604-r8-shrink-crashes branch July 31, 2026 23:57
hal-eisen-adfa added a commit that referenced this pull request Aug 15, 2026
* ADFA-5156: Roll back R8 shrinking to unbreak plugins

Restores the blanket -dontshrink that ADFA-3604 (#1596) removed on
2026-07-29. This is a temporary rollback to restore plugin functionality;
a targeted fix follows.

Plugins are loaded parent-first through a stock DexClassLoader
(PluginLoader.kt:92-116, parent passed at PluginManager.kt:603), so every
kotlin.** class a plugin references resolves from the IDE's dex, not from
the ~1058 stdlib classes the plugin bundles. R8 cannot see plugin call
sites, so it strips every stdlib member the IDE itself does not call. The
net effect is that a plugin can only call the subset of the Kotlin standard
library that the IDE also calls; anything else throws NoSuchMethodError at
runtime. Sketch to UI fails on every image load with
"No static method maxOrNull([F)Ljava/lang/Float; in class ArraysKt".

-dontobfuscate and -dontoptimize were already set, so restoring -dontshrink
reduces R8 to a pass-through and returns the release build to the
configuration shipped before ADFA-3604. isMinifyEnabled and
isShrinkResources are deliberately left alone, keeping resource shrinking
and the build wiring unchanged.

Verified by dex-scanning both APKs (baseline pulled from a release install
on Samsung RFCT704HEAL):

  kotlin/kotlinx method declarations   30,669 -> 45,371
  ArraysKt/CollectionsKt/MapsKt/
    FilesKt/SequencesKt facades        absent -> present
  10 sketch-to-ui stdlib call sites    all stripped -> all present
  CompletableJob$DefaultImpls.plus     stripped -> present

Sketch to UI now loads an image and completes detection on-device with no
NoSuchMethodError in logcat.

APK size: 659,307,160 -> 706,829,817 bytes (+47.5 MB, +7.2%).

Note: R8 was buying less than #1596 advertised. That PR measured dex at
28.8 MB / 24,853 classes, but the shipped APK is 85 MB / 78,757 classes --
the URGENT follow-ups (#1609, #1610) added -dontoptimize plus a set of keep
rules that clawed most of it back.

* ADFA-5156: Add R8 plugin-impact analysis tooling

The ADFA-5156 failure mode is invisible at build time -- assemblePlugin is
green, the manifest is fine, the .cgp is correct, and only on-device
execution of a specific code path reveals that R8 stripped a stdlib member
the plugin needs. These scripts make it measurable from build artifacts
instead.

scripts/r8-plugin-impact/
  README.md                  what the bug is, how to run, how to read output,
                             the known false positives, and the ADFA-5156
                             baseline numbers to measure future builds against
  dex-dump.sh                extract + dexdump an APK or .cgp
  analyze-plugin-impact.py   simulate parent-first resolution of every
                             kotlin.*/kotlinx.* call site in each plugin's own
                             code against two host dexes and diff the verdicts

Three subcommands: impact (the before/after table), explain-method (trace one
resolution chain, showing where it leaves the APK), explain-absent (inspect
fall-throughs for the split-brain shape that caused the original bug).

Documents three traps that produce wrong conclusions if analysis is done ad
hoc, all of which bit during this investigation:

  - Methods inherited from the Android boot classpath read as missing, because
    java.util.* is not in the APK. Eight such false positives are enumerated.
  - Kotlin multifile facades (ArraysKt, StringsKt) declare nothing themselves;
    they extend a part class whose underscore count varies
    (StringsKt__StringsKt vs ArraysKt___ArraysKt). Checking a facade directly
    always fails.
  - D8 build-time synthetics ($$ExternalSyntheticBackport0 and friends) never
    exist in the host and always show as absent.

Stdlib-only Python, no third-party dependencies. Run with
uv run --no-project.

* ADFA-5156: Apply spotless formatting to plugin-impact scripts

* ADFA-5156: Point plugin-impact baseline at the deployed plugin set

The first baseline measured a local folder of .cgp files that turned out to
be a pre-rename snapshot -- 5 stale filenames and 3 plugins missing. Re-runs
against the artifact from the last update-libs.yml deploy (26 plugins, 4,261
call sites) and documents how to obtain that artifact, so the next person
does not measure the wrong set. Conclusion is unchanged: zero regressions,
67 real failures to 0.
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.

1 participant