Skip to content

perf(r8): drop the protobuf and gRPC server keep rules - #1370

Merged
bmc08gt merged 3 commits into
code/cashfrom
perf/narrow-protobuf-keep-rules
Aug 31, 2026
Merged

perf(r8): drop the protobuf and gRPC server keep rules#1370
bmc08gt merged 3 commits into
code/cashfrom
perf/narrow-protobuf-keep-rules

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

-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. 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
*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

R8 configuration analyzer, both columns on this branch against the published 0.3.0 packages.
The baseline is efbe3e700's proguard-rules.pro and libs.versions.toml checked out over
the same tree, so nothing but the rules differs:

live methods    230,339 -> 204,797   (-25,542)
live classes     42,020 ->  41,452      (-568)
live fields      96,926 ->  95,679    (-1,247)

obfuscation score  89.1% -> 96.2%
optimization score 89.3% -> 96.4%
shrinking score    89.3% -> 96.4%

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.txt shows both being read.

395 message classes keep their fields, including the payment path
(SubmitIntentRequest$SubmitActions, SubmitSignatures), account info, and the messaging
streams.

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 likely
removable 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.toml moves both pins to 0.3.0 in the same commit that
drops 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.

`-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.
…obuf-keep-rules

* origin/code/cash:
  perf(r8): keep exception names without keeping every exception (#1373)
  fix(bugsnag): give the app and the uploaded mapping the same build UUID (#1374)
  chore(r8): drop R8 options that no longer do anything (#1371)
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.
@github-actions github-actions Bot added the area: build-system Gradle, convention plugins, build-logic label Aug 31, 2026
@bmc08gt
bmc08gt merged commit 1a46bb8 into code/cash Aug 31, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the perf/narrow-protobuf-keep-rules branch August 31, 2026 13:59
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: build-system Gradle, convention plugins, build-logic

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant