perf(r8): drop the protobuf and gRPC server keep rules - #1370
Merged
Conversation
`-keep class * extends GeneratedMessageLite { *; }` and its $Builder twin pinned
29,633 items — 94% of the app's entire keep radius — and blocked R8 from optimizing
24,909 methods, 81% of every method it was forbidden to touch. Both are replaced by a
field-only rule that now ships inside com.flipcash:{ocp,flipcash2}-client-protocol,
where the generated messages live. javalite resolves fields reflectively and reaches
builders and message methods from ordinary call sites, so the methods never needed
keeping.
`-keep class * implements io.grpc.BindableService { *; }` goes with them. All 45
matches were *ImplBase and *CoroutineImplBase server skeletons generated alongside the
client stubs; nothing in an Android client implements a gRPC service, and no other rule
kept them, so R8 can now delete them outright.
Measured by the R8 configuration analyzer, before and after:
live methods 230,686 -> 205,144 (-25,542)
live classes 42,338 -> 41,770 (-568)
live fields 96,939 -> 95,692 (-1,247)
obfuscation score 89.0% -> 96.1%
optimization score 89.3% -> 96.3%
shrinking score 89.1% -> 96.1%
The AbstractStub rule stays for now. It pins 114 classes with all members and the stubs
are reached from generated newStub() factories, so it is very likely removable too, but
that one needs a run on a device rather than a static argument.
Merges after both contract packages publish the rule; the pinned versions in
libs.versions.toml have to carry it first.
This was referenced Aug 30, 2026
0.3.0 is the first release of either package to ship META-INF/proguard/*.pro alongside its generated messages, which is what makes the removal of the app's own protobuf rules in this branch safe. Both jars on Central carry the rule; R8 reads it straight out of the dependency. Pinning the version and dropping the local rules in one change keeps the app from ever sitting on a commit with no protobuf keep rule at all.
bmc08gt
added a commit
that referenced
this pull request
Aug 31, 2026
) * fix(nav): keep NavKey classes so screen-root test tags survive R8 `annotatedEntry` derives each screen's root test tag from `T::class.simpleName` (NavMetadata.screenRootTag). `getSimpleName()` on a nested class reads the `InnerClasses` attribute, and R8 emits that attribute only for classes matched by a full -keep. Under the two `-keepnames class com.flipcash.app.core.AppRoute...` rules the binary name survived but the attribute did not, so `AppRoute.Main.Scanner` reported `AppRoute$Main$Scanner` and its tag came out as `app_route$main$scanner_screen`. The global `-keepattributes InnerClasses`, which Retrofit's bundled rules already pull in, does not change that. The rules also never matched `com.flipcash.app.core.onboarding.OnboardingStep`, which reaches `NavKey` through `FlowStep` rather than `AppRoute`. Those classes were obfuscated outright, so the seed screen reported `n69_screen`. That second case is also why reading the binary name in Kotlin would not have been a fix. Matching on the `NavKey` supertype covers both hierarchies and any future one. Class-only, with no member wildcard, so members stay shrinkable and renameable. Cost against the current baseline is +137 classes retained and +16 KB of APK; for scale, #1370 removed 568 classes and 25,542 live methods. On a minified debug build (DEBUG_MINIFY=true, which leaves UI_TESTABLE true), `uiautomator dump` now reports `wallet_screen` and `scanner_screen` — both nested under `AppRoute.Main`. In the same APK's dex, `OnboardingStep$SeedInput` carries `InnerClass name="SeedInput"`, the member `getSimpleName()` reads. Only builds with `BuildConfig.UI_TESTABLE` true expose testTags as resource-ids, so a stock release never showed these tags. The gain is that Maestro can run against a minified build. * ci(maestro): run the E2E suite against a minified build Nothing in the workflow set DEBUG_MINIFY, so Maestro only ever ran against an unminified debug build. A keep rule that stops protecting the screen-root test tags would pass CI and only show up when someone ran the suite locally on a minified build. Set DEBUG_MINIFY from a matrix entry, defaulting to true, so the nightly run is the one that exercises R8. A `minify` dispatch input takes true, false, or both; `both` expands to a two-entry matrix through a small setup job, since a matrix cannot be built from an input inline. max-parallel is 1 because every flow signs into the same shared test account. Reports are uploaded per variant. * fix(maestro): name the missing credential instead of failing on an assertion An unset credential was not an error anywhere: cred() returned an empty string, run.sh passed it to Maestro as an empty -e value, `inputText: ${SEED_PHRASE}` typed nothing, and the run failed twenty minutes later on `wallet_screen is visible` — an assertion inside a login subflow that names none of the cause. The nightly CI job has failed that way on every run since it was added, because the MAESTRO_* repo secrets it maps do not exist and each expanded to "". Check the credentials before touching the device. A missing SEED_PHRASE or LOGIN_DEEPLINK stops the run, since between them they gate every flow; the flow-specific ones warn and name the flows that read them, because a local run of one flow has no reason to set the rest. Names only, never values. Also fix seed_contact dying silently: pipefail turns a no-match grep into a failed assignment, which set -e acts on before the empty-rid guard below it can report anything. It fires only when the raw_contacts query comes back empty, so a real emulator does not hit it. LOGIN_USERNAME had no mapping in the workflow, so the vanity flows would have warned on every CI run. Added it.
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.
-keep class * extends GeneratedMessageLite { *; }and its$Buildertwin pinned 29,633items — 94% of the app's entire keep radius — and blocked R8 from optimizing 24,909 methods,
81% of every method it was forbidden to touch. Two rules, three lines, and R8 was working on
a fraction of the app.
They are replaced by a field-only rule that now ships inside the contract packages, where the
generated messages live. javalite resolves fields reflectively but reaches builders and
message methods from ordinary call sites, so the methods never needed keeping.
-keep class * implements io.grpc.BindableService { *; }goes with them. All 45 matches were*ImplBaseand*CoroutineImplBaseserver skeletons generated alongside the client stubs.Nothing in an Android client implements a gRPC service, and no other rule kept them, so R8
can now delete them outright.
Measured
R8 configuration analyzer, both columns on this branch against the published 0.3.0 packages.
The baseline is
efbe3e700'sproguard-rules.proandlibs.versions.tomlchecked out overthe same tree, so nothing but the rules differs:
The keep radius is where it shows. The two dropped rules pinned 29,633 items between them;
the field-only rule that replaces them pins 3,072, and the total radius across every rule in
the build falls from 42,195 to 15,089.
Both packages ship byte-identical rules, so R8 collapses them into one radius entry credited
to flipcash2.
configuration.txtshows both being read.395 message classes keep their fields, including the payment path
(
SubmitIntentRequest$SubmitActions,SubmitSignatures), account info, and the messagingstreams.
What stays
-keep class * extends io.grpc.stub.AbstractStub { *; }pins 114 classes with all members,and the stubs are reached from generated
newStub()factories, so it is very likelyremovable too. That one needs a run on a device rather than a static argument, so it goes
with the other narrowing work.
Ordering
Self-contained now.
libs.versions.tomlmoves both pins to 0.3.0 in the same commit thatdrops the rules, so no commit on this branch is left without a protobuf keep rule.
Contract packages: ocp-client-protocol#5,
flipcash2-client-protocol#6 — both
merged and published as 0.3.0.