Repository navigation
Conversation
…GP 9.2.1 baseline
| from typing import Any | ||
|
|
||
| DEFAULT_AAB = "app/build/outputs/bundle/release/app-release.aab" | ||
| FALLBACK_AAB = "app/app-release.aab" |
There was a problem hiding this comment.
I don't understand the reason to have both a default and a fallback.
There was a problem hiding this comment.
Agreed, there wasn't a good reason. The fallback was left over from a local demo setup. Removed it in be01dc1; there's now one default path.
| parser.add_argument( | ||
| "--aab", | ||
| type=Path, | ||
| default=None, |
There was a problem hiding this comment.
You should set default=Path(DEFAULT_AAB).
| subprocess.run(["./gradlew", ":app:bundleRelease"], check=True) | ||
|
|
||
| aab_path = args.aab | ||
| if aab_path is None: |
There was a problem hiding this comment.
If you set a default, this can just be if not aab_path.exists(): and you can say "No AAB found at {aab_path}"
There was a problem hiding this comment.
Done in be01dc1: it's now a single if not args.aab.exists() check that says "No AAB found at {path}".
| ) | ||
| return 2 | ||
|
|
||
| print(f"{BOLD}==> Running `ANDROID_CLI_ANALYZE=1 android analyze` on {aab_path}...{RESET}") |
There was a problem hiding this comment.
ANDROID_CLI_ANALYZE=1 is no longer necessary.
There was a problem hiding this comment.
Removed in be01dc1, here and in the other scripts and the workflow.
| def run_android_analyze(artifact_path: Path) -> dict[str, Any]: | ||
| """Invokes `android analyze --analysis=optimization --format=json` on an AAR/JAR.""" | ||
| env = os.environ.copy() | ||
| env["ANDROID_CLI_ANALYZE"] = "1" |
|
|
||
|
|
||
| def _unwrap_optimization(data: dict[str, Any]) -> dict[str, Any]: | ||
| """Supports both `{"results": [{"optimization": ...}]}` and `{"optimization": ...}`.""" |
| } | ||
|
|
||
|
|
||
| def format_kb(num_bytes: int) -> str: |
There was a problem hiding this comment.
function name is misleading, it formats bytes to either MB and KB
| r8_bytes = int(r8_compile.get("dexSizeBytes", 0)) | ||
|
|
||
| keep_ratio_pct = (r8_bytes / d8_bytes * 100.0) if d8_bytes > 0 else 0.0 | ||
| dex_shrinking_pct = 100.0 - keep_ratio_pct if d8_bytes > 0 else 100.0 |
There was a problem hiding this comment.
Why both the keep % and shrinking %?
There was a problem hiding this comment.
No need for both. In be01dc1 I dropped the keep ratio; only the shrinking % is reported.
| fld_total, fld_kept, fld_shrunk_pct = _item_stats("fields") | ||
|
|
||
| validate = dex.get("validateConsumerKeepRulesResult", {}) | ||
| used_pkg_wide = int(validate.get("usedPackageWideKeepRules", 0)) |
There was a problem hiding this comment.
I think you want len() here and below instead to get the count of rules.
There was a problem hiding this comment.
Yes, thanks. Checking it against the real android analyze output turned up more problems in this function: the package-wide rule lists are under r8CompileResult.configuration, not validateConsumerKeepRulesResult; the kept counts are keptItemCount, not keptCount; and blocked rules use source, not value. All fixed in be01dc1. The watchface AAR now reports its 11 package-wide consumer rules (WARN) instead of 0.
| def run_android_analyze(artifact_path: Path) -> dict[str, Any]: | ||
| """Invokes `android analyze --analysis=optimization --format=json` on an AAB/APK/AAR/JAR.""" | ||
| env = os.environ.copy() | ||
| env["ANDROID_CLI_ANALYZE"] = "1" |
There was a problem hiding this comment.
Removed the whole file in be01dc1. It was the old all-in-one script, replaced by the three split scripts.
- check_library_shrinking.py: read the keys `android analyze` actually outputs (keptItemCount, configuration.used/unusedPackageWideKeepRules, blockedKeepRules[].source) and count rules with len(). Package-wide consumer rules were always reported as 0 before (watchface has 11). Drop the redundant keep ratio and rename format_kb to format_size. - check_app_r8_score.py: single default AAB path, drop --build and the old JSON shape, read the package name from --analysis=basic. - Remove ANDROID_CLI_ANALYZE (no longer needed) and --no-configuration-cache from the scripts and workflow. - Always run the git hooks, the pre-commit config hooks and the CI workflow instead of filtering by changed files. - Post one PR comment and edit it on later runs. - Remove check_r8_regression.py, superseded by the split scripts.
🛡️ R8 Optimization & Shrinking Gate (
|
| Metric | Baseline | Current | Delta | Threshold | Status |
|---|---|---|---|---|---|
| Shrinking Score | 51.15% | 51.64% | +0.49% pts |
max -2.00% pts |
✅ PASS |
| Optimization Score | 50.81% | 51.30% | +0.49% pts |
max -2.00% pts |
✅ PASS |
| Obfuscation Score | 51.11% | 51.61% | +0.50% pts |
max -2.00% pts |
✅ PASS |
| Uncompressed DEX Size | 18.32 MB | 17.53 MB | -0.79 MB (-4.3%) |
max +5.0% / +500KB |
✅ PASS |
📚 Library Shrinking Percentage (Min Threshold: 90.0%)
Library (.aar / .jar) |
D8 DEX | R8 Kept DEX | DEX Shrinking % | Classes Shrunk | Methods Shrunk | Pkg-Wide Rules | Status |
|---|---|---|---|---|---|---|---|
network-release.aar |
64.35 KB | 0.00 KB | 100.00% | 100.0% (49/49) | 100.0% (250/250) | 0 | ✅ PASS |
testing-release.aar |
29.22 KB | 0.00 KB | 100.00% | 100.0% (17/17) | 100.0% (119/119) | 0 | ✅ PASS |
theme-release.aar |
209.49 KB | 0.00 KB | 100.00% | 100.0% (51/51) | 100.0% (405/405) | 0 | ✅ PASS |
util-release.aar |
44.71 KB | 0.00 KB | 100.00% | 100.0% (24/24) | 100.0% (111/111) | 0 | ✅ PASS |
xr-release.aar |
44.73 KB | 0.00 KB | 100.00% | 100.0% (13/13) | 100.0% (64/64) | 0 | ✅ PASS |
data-release.aar |
55.13 KB | 0.00 KB | 100.00% | 100.0% (54/54) | 100.0% (193/193) | 0 | ✅ PASS |
camera-release.aar |
289.75 KB | 0.00 KB | 100.00% | 100.0% (93/93) | 100.0% (550/550) | 0 | ✅ PASS |
creation-release.aar |
343.21 KB | 0.00 KB | 100.00% | 100.0% (86/86) | 100.0% (610/610) | 0 | ✅ PASS |
home-release.aar |
186.55 KB | 0.00 KB | 100.00% | 100.0% (47/47) | 100.0% (310/310) | 0 | ✅ PASS |
results-release.aar |
602.10 KB | 0.00 KB | 100.00% | 100.0% (200/200) | 100.0% (1127/1127) | 0 | ✅ PASS |
watchface-release.aar |
72.20 KB | 2.73 KB | 96.21% | 95.7% (44/46) | 96.1% (246/256) | 11 | |
common-release.aar |
45.32 KB | 0.00 KB | 100.00% | 100.0% (31/31) | 100.0% (229/229) | 0 | ✅ PASS |
On every PR, post a GitHub-style diff of the keep rules that changed compared with main, and the classes they keep. It covers rules that come with library upgrades and existing wildcard rules that start keeping new classes, neither of which shows up in the PR's own diff. It doesn't build anything. With AGP 9.3.2, every release build writes the keep rule .pb file (app/build/outputs/mapping/release/configanalyzer.pb) by default, so the gate uploads the file from the build it already runs. - keep_rule_diff.yml: reusable workflow, given the .pb file's path. On pushes to main it caches the file per commit; on PRs it diffs against main's cached file, writes the job summary and keeps one PR comment up to date. - r8_regression_check.yml: upload the .pb file after the release build and call keep_rule_diff.yml in a second job, which also runs when the gate fails. Optionally skip the diff when the R8 scores didn't change (KEEP_RULE_DIFF_ONLY_IF_SCORES_CHANGED). - scripts/keep_rule_diff.py and scripts/schema_pb2.py: compare two keep rule .pb files and write the Markdown report. Needs AGP 9.3 or newer to write the .pb file; until then the diff is skipped.
zizmor's excessive-permissions audit flags workflow-level write permissions once a workflow has more than one job, which the new keep-rule-diff job made it. Move them to the r8-regression-gate job; keep-rule-diff already sets its own (contents: read, pull-requests: write).
main doesn't run the keep rule diff yet, so the cache has no keep rules from main to compare this PR against. A temporary job builds the PR's base commit once, with AGP 9.3.2 so that it writes the keep rule .pb file, and caches it under the key the keep-rule-diff job looks for. To be removed once the diff has run.
🔍 R8 keep rule diff
@@ app/proguard-rules.pro @@
- -keep class com.firebase.** { *; }
- -keep interface com.firebase.** { *; }
- -keep class okhttp3.** { *; }
- okhttp3.Address
- okhttp3.Authenticator
- okhttp3.Authenticator$Companion
- okhttp3.Authenticator$Companion$AuthenticatorNone
- okhttp3.Cache
- okhttp3.Cache$CacheResponseBody
- okhttp3.Cache$CacheResponseBody$1
- okhttp3.Cache$Companion
- okhttp3.Cache$Entry
- okhttp3.Cache$Entry$Companion
- okhttp3.Cache$RealCacheRequest
- okhttp3.Cache$RealCacheRequest$1
- okhttp3.Cache$urls$1
- okhttp3.CacheControl
- okhttp3.CacheControl$Builder
- okhttp3.CacheControl$Companion
- okhttp3.Call
- okhttp3.Call$Factory
- okhttp3.Callback
- okhttp3.CertificatePinner
- okhttp3.CertificatePinner$Builder
- okhttp3.CertificatePinner$Companion
- okhttp3.CertificatePinner$Pin
- okhttp3.Challenge
- okhttp3.CipherSuite
- okhttp3.CipherSuite$Companion
- okhttp3.CipherSuite$Companion$ORDER_BY_NAME$1
- okhttp3.CompressionInterceptor
- okhttp3.CompressionInterceptor$DecompressionAlgorithm
- okhttp3.Connection
- okhttp3.ConnectionPool
- okhttp3.ConnectionSpec
- okhttp3.ConnectionSpec$Builder
- okhttp3.ConnectionSpec$Companion
- okhttp3.Cookie
- okhttp3.Cookie$Builder
- okhttp3.Cookie$Companion
- okhttp3.CookieJar
- okhttp3.CookieJar$Companion
- okhttp3.CookieJar$Companion$NoCookies
- okhttp3.Credentials
- okhttp3.Dispatcher
- okhttp3.Dispatcher$promoteAndExecute$Effects
- okhttp3.Dns
- okhttp3.Dns$Companion
- okhttp3.Dns$Companion$DnsSystem
- okhttp3.EventListener
- okhttp3.EventListener$AggregateEventListener
- okhttp3.EventListener$Companion
- okhttp3.EventListener$Companion$NONE$1
- … and 323 more
- -keep interface okhttp3.** { *; }
- okhttp3.Authenticator
- okhttp3.Call
- okhttp3.Call$Factory
- okhttp3.Callback
- okhttp3.CompressionInterceptor$DecompressionAlgorithm
- okhttp3.Connection
- okhttp3.CookieJar
- okhttp3.Dns
- okhttp3.EventListener$Factory
- okhttp3.Interceptor
- okhttp3.Interceptor$Chain
- okhttp3.TrailersSource
- okhttp3.WebSocket
- okhttp3.WebSocket$Factory
- okhttp3.internal.SuppressSignatureCheck
- okhttp3.internal.cache.CacheRequest
- okhttp3.internal.concurrent.Lockable
- okhttp3.internal.concurrent.TaskRunner$Backend
- okhttp3.internal.connection.BufferedSocket
- okhttp3.internal.connection.ExchangeFinder
- okhttp3.internal.connection.RoutePlanner
- okhttp3.internal.connection.RoutePlanner$Plan
- okhttp3.internal.http.ExchangeCodec
- okhttp3.internal.http.ExchangeCodec$Carrier
- okhttp3.internal.http2.FlowControlListener
- okhttp3.internal.http2.Http2Reader$Handler
- okhttp3.internal.http2.PushObserver
- okhttp3.internal.platform.ContextAwarePlatform
- okhttp3.internal.platform.android.DeferredSocketAdapter$Factory
- okhttp3.internal.platform.android.SocketAdapter
- okhttp3.internal.publicsuffix.PublicSuffixList
- okhttp3.internal.sse.ServerSentEventReader$Callback
- okhttp3.internal.tls.TrustRootIndex
- okhttp3.internal.ws.WebSocketReader$FrameCallback
- okhttp3.logging.HttpLoggingInterceptor$Logger
- okhttp3.sse.EventSource
- okhttp3.sse.EventSource$Factory |
cwoodwar6
left a comment
There was a problem hiding this comment.
I'm not sure if having the keep rule diff is worth the added proto complexity. It is nice to have when a library changes a keep rule, but for anything changed within the project, those changes are already visible in the git diff.
There was a problem hiding this comment.
Should avoid checking in this file. Do you have the .proto file? Adding a proto dependency seems a bit risky, I'd avoid it if possible.
|
|
||
| python3 scripts/keep_rule_diff.py base.pb pr.pb -o keep_rule_diff.md | ||
|
|
||
| Reading .pb files needs `pip install 'protobuf>=6.33.4'` and schema_pb2.py next to this script. |
There was a problem hiding this comment.
This dependency should be installed in your workflow yaml.
Adds Automated presubmit (Git hooks) and CI (GitHub Actions) regression gates powered by
android analyzeto catch R8 configuration regressions, unshrunk libraries, and DEX size bloat before merge.1. Prerequisites & Setup Requirements
To adopt this workflow in a repository, the following baseline file, secrets, build settings, and tools are required:
A. Initial R8 Baseline File (
r8-baseline.json)check_app_r8_score.pycompares every PR's release App Bundle (.aab) against a committedr8-baseline.jsonfile in the repository root. This file stores the referenceandroid analyzeoutput (shrinkingRatio,optimizationRatio,obfuscationRatio, anduncompressedDexSizeBytes).r8-baseline.jsonwhenever a passing PR merges tomain.B. GitHub Actions Secrets & Permissions
Configure the following in Settings → Secrets and variables → Actions:
GEMINI_API_KEY(Required for AI Diagnosis on Failure):google-github-actions/run-gemini-clistep when either the App Bundle gate or Library Shrinking gate fails.r8-analyzerskill) to inspect the PR diff (.gemini/pr_diff.patch) and comment with the exact keep rule or dependency that caused the regression.GITHUB_TOKEN/ OptionalPAT):contents: write,pull-requests: write, andissues: writepermissions so it can post scorecard comments on PRs (gh pr comment) and auto-commit updatedr8-baseline.jsonfiles onmain.PATSecret: If yourmainbranch has branch protection rules that blockgithub.tokenfrom pushing commits, add a Personal Access Token secret namedPATwithrepo/contents: writescope.DEBUG_GOOGLE_SERVICES_JSON_BASE64)::app:bundleReleasein CI (in Androidify, ifDEBUG_GOOGLE_SERVICES_JSON_BASE64is not set, the workflow falls back to copyingtest-google-services.jsontoapp/google-services.json).C. Android Gradle Plugin (AGP) & R8 Build Configuration
isMinifyEnabled = true):app/build.gradle.kts, thereleasebuild type must haveisMinifyEnabled = true.BUNDLE-METADATA/com.android.tools/r8.jsoninsideapp-release.aab, whichandroid analyzereads to calculate shrinking, optimization, and obfuscation scores.consumerProguardFiles):check_library_shrinking.py --scan-libraries), library modules must build release AARs (./gradlew assembleRelease) soandroid analyzecan inspect their packagedproguard.txtconsumer rules and run isolated D8 vs. R8 shrinking comparisons.D. Android CLI & Agent Skill
android):https://dl.google.com/android/cli/latest/linux_x86_64/install.sh(or locally on macOS viadarwin_arm64/install.sh), providing theandroid analyze --analysis=optimizationcommand.r8-analyzerSkill:android skills add --agent=gemini --project=. r8-analyzer, providing the domain instructions at.agents/skills/r8-analyzer/SKILL.mdfor root-cause keep-rule analysis.2. Modular Check Scripts (
scripts/)Teams can adopt all three checks or pick only the ones that fit their workflow:
check_staged_r8_diff.pygit diff,*.pro,libs.versions.tomlenableR8.fullMode=false, newly added-keep class <pkg>.** { *; }rules, and audits newly added Maven dependencies in~/.gradle/cachespre-commit(<1s)check_library_shrinking.py.aar&.jarmodules/libraries(1 - r8DexSize / d8DexSize) * 100) and flags consumerproguard.txtrules with package-wide wildcardspre-commit/ CIcheck_app_r8_score.py.aab)shrinkingRatio,optimizationRatio,obfuscationRatio(0–100) anduncompressedDexSizeBytesagainstr8-baseline.jsonpre-push/ CI3. GitHub Actions Workflow (
.github/workflows/r8_regression_check.yml)Triggers & Inputs
pull_request/push(main): Runs automatically whenever ProGuard/R8 files (**/*.pro), Gradle build scripts (**/build.gradle.kts,gradle/libs.versions.toml,gradle.properties),r8-baseline.json, orscripts/check_*.pychange.workflow_dispatch: Supports manual execution against any PR or commit with configurable thresholds:pr_number: Optional PR number to checkout and comment on.max_score_drop(default2.0): Maximum allowed drop in R8 shrinking, optimization, or obfuscation scores (percentage points).max_dex_growth_pct(default5.0): Maximum allowed uncompressed DEX size increase (%).min_lib_shrinking_pct(default90.0): Minimum required isolated DEX shrinking percentage for module AARs (%).Workflow Pipeline Steps
.pro,.gradle.kts,libs.versions.toml, andgradle.propertieschanges into.gemini/pr_diff.patch.androidCLI (https://dl.google.com/android/cli/latest/linux_x86_64/install.sh) and adds ther8-analyzerskill (android skills add --agent=gemini --project=. r8-analyzer)../gradlew :app:bundleRelease assembleReleaseto produce the release App Bundle (app-release.aab) and module AARs (*-release.aar).app_r8_gate): Runscheck_app_r8_score.pyto compare the release.aabagainstr8-baseline.jsonand writes the scorecard to.gemini/r8_summary.mdand$GITHUB_STEP_SUMMARY.lib_r8_gate): Runscheck_library_shrinking.py(--scan-libraries --append-summary) across built module AARs to verify isolated R8 shrinking ratios and flag package-wide consumer keep rules.main: Onpushtomainwhen both gates pass, runscheck_app_r8_score.py --save-baselineand auto-commits updated metrics tor8-baseline.json.gemini_review): If either gate fails, invokesgoogle-github-actions/run-gemini-cliwith ther8-analyzerskill to inspect.gemini/pr_diff.patchand pinpoint the exact keep rule or dependency causing the regression.r8-analyzerremediation recommendations directly to the PR viagh pr comment, uploadsr8-config-analyzer-report(r8-config-analyzer-release.html), and fails the check if any regression threshold was breached.4. Presubmit Git Hooks (
scripts/git-hooks/&.pre-commit-config.yaml)scripts/git-hooks/pre-commit: Runscheck_staged_r8_diff.pyon staged changes andcheck_library_shrinking.pyon built module AARs.scripts/git-hooks/pre-push: Runscheck_app_r8_score.pyagainstr8-baseline.jsonwhen a release.aabis present..pre-commit-config.yaml: Exposes three independent hook IDs (r8-staged-diff-and-deps,r8-library-shrinking-gate,r8-app-bundle-score-gate) so teams usingpre-commitcan enable individual checks.