Testing and release evidence

R8 and ProGuard Tests for Android Local AI

Compare debug and minified release behavior across reflection, JNI registration, serialization, Room, model loading, and failure paths.

Review status: Fact and editorial QA complete; owner review pending · 2026-10-05 · Fact IDs: COVE-LIFECYCLE-01, BENCHMARK-01

Direct answer

R8 can change names, remove code, and optimize program structure; failures may appear only in a minified release when code is reached through JNI upcalls or reflection that static analysis cannot see. Android’s keep-rule examples explain that native code calling Java/Kotlin methods is invisible to R8 unless rules preserve the required classes and members. A failure can surface as a missing method, class lookup problem, or behavior change, but it is not automatically a missing native library or bad model artifact. Compare the exact debug and release paths before adding rules. Pin the application build variant, AGP/R8 configuration, runtime artifact, ABI packaging, mapping file, and the specific bridge entry points. Capture the full release exception and stage: library load, JNI registration, native-to-managed callback, reflective construction, serialization, model initialization, or task execution. R8 mapping files can help relate obfuscated names to source; preserve them per release. Do not publish or log them indiscriminately because they may expose code structure, and never use a blanket keep-all rule just to make the crash disappear. Create a small fixture for each dynamic boundary used by the app. Call native methods from Kotlin, exercise any native callbacks, reflectively instantiated classes, serializers, and annotated members that are part of the real integration. Build both debug and minified release variants with identical input fixtures. If debug passes and release fails, use the mapping and shrinker diagnostics to identify the exact removed or renamed symbol, then add the narrowest documented keep rule and rerun all affected tests. Do not assume that every class in a runtime dependency needs to be kept; library consumer rules and app-owned callbacks have different ownership. Separate R8 from native split/ABI delivery. If the library cannot be found or opened, inspect the APK or device-targeted split and ABI first. If the library loads but a callback or reflective method is missing only in release, examine R8 transformations. If the native call enters the method and then reports an exception, follow the JNI exception boundary; if the engine loads and output differs, test the artifact, tokenizer, and prompt separately. This sequence avoids broad keep rules that mask a different failure. Use controlled release evidence: store build variant, mapping file hash, keep-rule revision, artifact/runtime identity, device/OS, invoked entry point, failure stage, and outcome. Test both normal and failure paths, including cleanup and the next request. This article does not assert that Cove currently has an R8 problem, nor that a particular keep rule fixes every runtime. It provides a release-diagnostic method bounded to the code paths and artifacts actually tested.

Debug inference works, but a minified release misses a native callback

A local model initializes in debug. A release build fails after native code calls back into Kotlin, while the model artifact and target device appear unchanged. The team has not compared R8 mapping data or identified the first missing symbol.

Decision

Compare equivalent debug and minified artifacts, inspect mapping and release diagnostics, then add a narrow keep rule only for the proven dynamic entry point.

Expected outcome

The team can separate shrinker removal/renaming from ABI split, JNI exception, and model-load failures without shipping a broad keep-all rule.

Evidence to collect

The claim becomes reviewable only when the following evidence is attached to the same artifact and test run:

  • Release mapping and keep-rule record
  • Debug-versus-release fixture set
  • Load, task, migration, and crash outcomes

A release claim is valid only for the recorded configuration and acceptance set. Generalization requires additional records.

Implementation workflow

  1. Freeze debug and release identities. Record app build variant, AGP/R8 version and settings, runtime dependency, ABI, artifact, release mapping file, and exact failing task. Keep the same fixture and model inputs across variants.
  2. Locate the dynamic entry point. Identify native-to-managed callbacks, reflection-loaded classes, serializers, or methods addressed by name. Record the expected class/member/signature and the stage at which lookup fails.
  3. Compare failure stage before editing rules. Verify whether the native library loaded. Missing library/ABI belongs to package inspection; a release-only missing callback or renamed reflected member suggests shrinker behavior. Preserve the full exception and mapping file securely.
  4. Apply a narrow documented keep rule. Use official keep-rule guidance for only the class/member/signature that dynamic code needs. Avoid wildcard keep-all changes; keep rules may prevent optimization beyond the affected bridge.
  5. Run minified regression and recovery tests. Repeat native callback, reflection, initialization, task, exception, cleanup, and next-request fixtures on minified release. Preserve the R8 mapping and confirm debug behavior did not change unexpectedly.

Keep each transition observable. A failure should identify the stage, artifact, runtime, and recovery action without logging private user content.

Acceptance criteria

  • Debug and release records identify the same artifact/runtime/task and the release R8 mapping used for diagnosis.
  • The failure is assigned to shrinker transformation only when the native library loads and a required dynamic symbol is missing or renamed.
  • Any keep rule is narrowly tied to a documented JNI/reflection entry point and validated with a minified regression.
  • Conclusions do not conflate R8, ABI split packaging, JNI exception handling, model compatibility, or runtime performance.

Decision table

DimensionChoice or evidenceHow to interpret it
Native library not found/openedInspect APK/split ABI and linker dependency pathA packaging or library-load boundary; a keep rule may not address it.
Release-only missing callback/methodInspect R8 mapping and dynamic entry-point rulesPotential removal or renaming when JNI/reflection usage is invisible to static analysis.
Method found, then throwsInspect JNI exception contract and runtime stageThe symbol exists; follow exception and cleanup diagnostics rather than broadening keep rules.

Failure patterns to prevent

  • Adding broad keep-all rules
  • Testing only debug builds

Also prevent silent fallback, unpinned artifacts, missing cancellation, and conclusions that combine unlike configurations. Store unsuccessful runs alongside successful ones.

Minimum reproducibility record

LayerRecord
DeviceManufacturer, model, chipset, RAM class, operating-system build
SoftwareApplication version and git commit
ModelFamily, variant, revision, format, file length, hash, quantization
RuntimeName, revision, requested backend, observed backend evidence
WorkloadFixture revision, input hash, prompt hash, output policy
OutcomeCompleted, failed, cancelled, fallback, and privacy-safe diagnostics

Release checklist

  • ☐ The primary query is answered without an unsupported number.
  • ☐ Every artifact and runtime is pinned.
  • ☐ The representative task and failure policy are explicit.
  • ☐ Lifecycle, cancellation, cleanup, and fallback are tested.
  • ☐ User-content and network boundaries are documented.
  • ☐ Result wording applies only to the recorded configuration.
  • ☐ The page links to raw method or evidence when results are added.

Sources and related evidence

Chinese deployment and troubleshooting content is organized in the .奇连 AI 端侧专题.

Frequently asked questions

Should I add a keep-all rule if release inference crashes?

No. First identify whether a dynamic class or method was removed or renamed. Use a narrow keep rule for the proven JNI/reflection entry point and retest the minified build.

Does an R8 problem mean the native library is missing from the APK?

Not necessarily. A missing or incompatible library is a packaging/ABI issue. R8 can instead remove or rename managed members invoked dynamically; inspect the earliest failure and mapping.

Is a debug success enough to validate the release build?

No. Minification changes the artifact. Test the release variant, preserve its mapping file, and run the native callback, reflection, initialization, task, and recovery paths.