Apply caller-selected R8/ProGuard mappings to shared Android inspections, preserving raw identities, exact verification, ambiguity and provenance. Add retained summaries and bounded/cached parsing, with measured real R8/Conscrypt workflows and review fixes. Fixes #6329. Co-authored-by: 555aaditya <107977627+555aaditya@users.noreply.github.com>
24 KiB
Caller-selected Android mapping investigation — 2026-10-27
Question, revisions and oracle
Apply a caller-selected local R8 mapping while preserving observed DEX identities, original mapping-derived names/types, overloads, uncertainty, both artifact identities and complete source Evidence. Inspect the requested inventory and method bodies, then export complete Evidence. APKs and native code are never executed, installed or modified.
Both controls use main 4e67bc399951768737eeaf539f7bc9fa8373b244:
- Before:
3a588662d916f801a8c444dd86f1c65c81c76bf9, the integrated #7137 mapping implementation, without parsed-index reuse or retained symbol summaries. This is a feature-equivalent optimization control, not a claim that unmodified main already supported mappings. - After:
6b65d3a4a1a8307417365b9a1e9060e607538aa3, bounded/cached mapping and explicit retained summary delivery. Later documentation changes do not alter the measured implementation. Following main’s experiment archive, the maintained harness/fixtures now live under tests and this report under docs/verification; the original measured scripts remain available at that pinned revision. - The original #7137 author is 555aaditya; integration retains their co-author credit and preserves #7379's shared manifest contracts/default-package tests.
The source fixture has four classes,
eight fields and fifteen methods, including constructors, overloads, Unicode
names, nested/default-package classes, arrays and renamed types in prototypes.
The same actual R8 invocation emits DEX and mapping. Handwritten source
expectations are checked against pre-minification javap -s -p; an independently
compiled dexlib2 reader inventories raw DEX identities without REA's parser,
bridge or JADX aliases. All four classes and fifteen selected bodies are checked
on each investigation. Seventeen valid method mapping records include three
line ranges for one constructor; they are not seventeen distinct DEX methods.
The public-library task transforms the shipped classes.jar from Conscrypt
Android 2.8.0 with actual R8. Its 949,641-byte mapping has 530 class, 1,263 field
and 10,342 method records. A separate D8 conversion of the original class files
provides the unminified descriptor oracle. Every selected NativeCrypto field
(28) and method (390), plus inner-class identity, is verified; bodies are selected
for EVP_PKEY_new_RSA, EVP_DigestSignInit, SSL_new, SSL_free and
SSL_get_error. These are static DEX-only ZIP containers, not runnable APKs.
D8 warned that a platform adapter's com.android.org.conscrypt.OpenSSLSocketImpl
was missing; complete dependency/runtime linkage is unverified. R8 additionally
used -dontwarn **, with shrinking/optimization disabled and name minification
allowed. Stable R8 did not consume an existing APK's DEX.
Pinned artifacts
| Artifact | Observed SHA-256 |
|---|---|
| R8 8.0.40 Maven JAR | ab1379835c7d3e5f21f80347c3c81e2f762e0b9b02748ae5232c3afa14adf702 |
| JADX MCP 0.7.1 JAR | 6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75 |
| Android 36 platform JAR | d9eb9da824d9e247a352f570f01e1169e725b2954bca9e283a71786c59b59f9a |
| Source fixture input JAR | 88f3d46018ac2effb166ceed79fdd6e8b58b6bb08d2649cd0aa4f90dc260260c |
| Source fixture DEX | 0b4c49cf3d706f5c2c4c15a10ff27dfbabb9b07f463d508f6f348eafb3245909 |
| Source fixture static APK | f8c0849978ed7eceeeb293223cf2139c1749f672148dce1e16d018fa727445c9 |
| Source fixture mapping | 5ca162587794af1bd1da00f79ee64fd4ecde6f7c91048ea5596260104df6e483 |
| Conscrypt 2.8.0 AAR | 752044c289bc98e2dcabc00a1f88ca158b3aa3f505b38f124552bf4b2a0902b1 |
| Conscrypt shipped classes JAR | 2cd9b6bc444cfca69048e8327383b7f5a13fc7978465e6e0fd5111a2000822f6 |
| Conscrypt R8 DEX | cd4d7eb9b05e1a84d064f7f9e4b33d68a8f92a9f58a6a62e4041972964ef6d27 |
| Conscrypt static APK | 9a0224400b91b030213e0f6ba63d899a4ad6333f930414c768ce88bbef423e91 |
| Conscrypt mapping | 67cd69e706afee50a1cc4e13bd9fd26056932c569d0ea7706dfe6fe8215a2e3c |
R8 publisher and independent official Bazel checksum pin are linked from the fixture README. Conscrypt was downloaded from Maven Central and verified against its publisher SHA-256 sidecar. No generated binary or mapping file is committed. JAR/ZIP timestamps can change a source rebuild's artifact hash; the table identifies these measured bytes.
Owning changes and safe bounds
The provider keeps one successful parsed index. Each call still performs a stable regular-file read and SHA-256 check. Only identical digest/length with fixed parser options reuses parsed facts; current path provenance comes from the new read. Changed content, including same-size/same-mtime rewrites, is reparsed; missing or cancelled input never falls back to stale facts. Idle retirement and close release the index. During replacement, old and new parsed indices can briefly coexist. APK/query results are not cached by this mechanism.
The domain parser scans lines without a whole-file split, accounts retained
strings/containers before admitting records, and admits parameter/metadata
expansion before split/JSON parsing. The fixed 64 MiB input, estimated 128 MiB
index and 8 MiB per-residual-class claim allowances are resource constraints,
not format claims or measured V8 heap caps. Replaced metadata, deduplicated
range splits and overwritten inline frames are not cumulatively billed.
Ambiguity scans count all alternatives but retain only eight stable candidates;
conflicting residual metadata stays ambiguous. A separate combined 8 MiB claim
budget covers actual queries including all inner classes; omitted claims become
explicit unknown, preserving every raw identity and source SHA. It is not a
whole-report/wire-byte cap.
Existing shared retained-Evidence projection now supports class/method summaries through both CLI and MCP. They preserve complete normalized facts and selected bodies, omitting duplicate raw RPC metadata from the inline response. Complete parent Evidence remains exportable. Default/complete delivery is compatible; summary is explicit, and CLI requires a non-overwriting portable output bundle. Whole summary views admit 1 MiB including context: an admitted one-MiB body may therefore require complete detail. Refusal preserves the MCP parent ID and is nonretryable for the same selection; no facts are silently truncated.
Completed-task measurements
Linux x86-64, Node 24.22.1/npm 11.16.0, Temurin JDK 21.0.12.1, actual JADX 0.7.1, one analysis worker, 1 GiB JVM heap and two active processors. Harness, server and children were restricted to CPUs 0/1; Node heap was 3 GiB. Expensive checks ran serially under an 8 GiB sampled family-RSS stop and a 4 GiB host-free memory guard. No guard stopped a completed task and no OOM occurred.
The public stdio MCP harness uses named live tool
contracts and Ajv 2020 validation of SDK-converted advertised outputs. All
correctness comparisons, hash reads, discovery and cleanup are included in the
whole-harness cost. First-provider calls use fresh processes/JVMs; OS file caches
were warm, and the host was shared. Conscrypt order was before/after/after/before.
Each small task makes 19 inspections and one proof export; each library task
makes six inspections and one proof export. Every exported parent is checked
against the exact inline normalized facts, source parameters and APK SHA, and
must retain raw calls. The complete/summary lanes use the same requested facts,
provider, options, source bytes, discovery families (android,session) and proof.
Peak memory is summed process-family RSS sampled every 100 ms, including client, server, JVM and children. Shared pages can be counted twice and short peaks can be missed; this is not PSS, kernel peak accounting or an enforced production heap measurement. CPU is completed-family user+system time.
| Cohort | First task s | Warm task s | Whole harness s | CPU s | Peak family MiB |
|---|---|---|---|---|---|
| small-before | 7.342 | 1.611 | 12.060 | 16.680 | 781.7 |
| small-after | 7.960 | 1.842 | 12.945 | 17.413 | 791.5 |
| conscrypt-before | 8.057 | 1.015, 0.893 | 13.264 | 17.774 | 920.0 |
| conscrypt-after | 6.975 | 0.833, 0.817 | 11.743 | 17.242 | 855.0 |
| conscrypt-after2 | 6.987 | 0.725, 0.748 | 11.561 | 16.553 | 872.6 |
| conscrypt-before2 | 7.046 | 0.917, 0.886 | 11.855 | 17.093 | 949.7 |
Warm Conscrypt task medians were 0.905 s before and 0.783 s after (about 14% lower across four warm samples per implementation). Cold/whole-task and memory samples vary; no universal startup or memory improvement is established. The small fixture pays extra summary/retention overhead: 12.060 to 12.945 seconds for the whole harness, with CPU 16.680 to 17.413 seconds. Explicit complete delivery remains available.
An earlier separate cache-only cohort used complete responses without proof export, on the same base. Conscrypt warm six-call medians fell from about 0.981 to 0.765 s; before ranges were 0.950–0.996 s and after 0.743–0.850 s. Whole harness samples were 12.861–13.876 to 11.641–11.647 s, CPU 16.916–18.297 to 16.454–16.893 s, and peak family 910.1–938.1 to 853.1–876.8 MiB. Small before/after whole costs were 18.493/16.110 s, 17.495/16.589 CPU s and 763.4/776.3 MiB. These samples motivated reuse but are not substituted for the completed proof-export task above.
Whole-task token estimates
android-mapping-costs.py executed with tiktoken 0.14.0/o200k_base. Text projection counts a fixed task question, all discovered name/description/input schemas once per investigation, every RPC argument and all text replies, including proof export. SDK transport additionally counts output schemas and structured-content duplicates. Exported file contents are checked offline rather than injected into model context; export IO/CPU/memory is included above. Prompt replay, host pruning/rendering, hidden prompts, protocol handshake/framing and actual model billing are unmeasured. Estimates are not billed usage. Each table row is one warm completed investigation.
| Cohort | Estimated text tokens | Estimated SDK transport tokens |
|---|---|---|
| small-before | 43,526 | 167,094 |
| small-after | 41,755 | 159,507 |
| conscrypt-before | 180,213 | 510,351 |
| conscrypt-after | 69,836 | 217,646 |
| conscrypt-after2 | 69,908 | 217,789 |
| conscrypt-before2 | 177,902 | 505,723 |
Conscrypt text estimates fall about 61%, from approximately 178–180k to 70k, while preserving all requested facts and complete proof. Small tasks improve only about 4%; summary context has its own cost. The cache-only cohort kept similar token counts (~172–174k library text tokens without proof export), revealing that parse caching alone did not address repeated raw class metadata. Full inventories still cost more context than a direct known-method summary.
Normalized investigation digests match exactly across all before/after lanes:
- Source fixture:
ec76d1e086c8561370fc1ed5dfef67bb88b185e8fb57c7e30afe9f1f153634eb. - Conscrypt:
7ebf70795dfc23bf2b0d0e813837325fa221f15560ca2384fdb06f26d69d78a5.
Post-review ownership and final workflow confirmation
Greptile found three concrete boundaries subsequently fixed in
160573cb36f478a088b8a5284fa571cd97dc9a86: translated prototypes could expand
before comparison; a warm request without a mapping could bypass a pending
mapping descriptor's cleanup; and timeout classification inspected the outer
cleanup wrapper. Type matching now compares incrementally within the observed
descriptor, retained file cleanup gates shared request admission and idle
retirement independently of engine cleanup, and the first interruption is
classified before cleanup composition. All 126 related cases pass serially:
55 parser, five file-reader, four first-cause/close outcomes, three cleanup,
52 session/idle/cleanup and seven MCP mapping cases.
A separate bounded diagnostic found and corrected source-string retention. On Node 24.22.1/V8 13.6.233.17-node.58, four small selected class/method results from four distinct mappings containing 10 MiB of ignored comments each retained 41,944,028 extra external string bytes through the actual immutable Evidence ledger. Clearing it released that backing; detached-string controls retained zero extra external bytes. The parser now owns admitted UTF-16 strings once, sharing section names and copying only retained outermost/deduplicated facts. It preserves lone surrogate code units and never clones the entire index or copies names per lookup. The identical post-fix ledger experiment retained zero extra external bytes; its RSS delta was 151,552 bytes, versus 62,898,176 before. These are engine-specific observations, not portable heap guarantees. The unchanged Evidence/ledger modules and changed parser are fingerprinted by the diagnostic. Before/after whole diagnostic costs were 2.995/2.582 seconds, CPU 4.976/4.382 seconds and peak sampled family RSS 145,903,616/113,287,168 bytes, with explicit 256 MiB old-space selection and a 1 GiB family guard.
Final equivalent Conscrypt investigations compare the integrated unoptimized
control 5ecc6ccc662678c00a45bd171f40798634774155 with fixed 160573cb3, both
on main e99b6c2c3bcc5f4f050b51b432145144d0d47dc0, using the same artifacts,
facts, seven calls including full proof export, and before/after/after/before
order. Every normal-fact digest remains
7ebf70795dfc23bf2b0d0e813837325fa221f15560ca2384fdb06f26d69d78a5.
The final source-fixture summary/proof/negative qualification also passes all
43 calls from the maintained harness path.
| Final cohort | First s | Warm s | Whole s | CPU s | Peak family MiB | Warm text estimate |
|---|---|---|---|---|---|---|
| conscrypt-before | 7.187 | 1.027, 1.006 | 12.370 | 17.307 | 917.7 | 177,855 |
| conscrypt-after | 7.376 | 0.813, 0.866 | 12.402 | 17.522 | 868.7 | 69,819 |
| conscrypt-after2 | 6.878 | 0.707, 0.726 | 11.439 | 16.383 | 883.4 | 69,902 |
| conscrypt-before2 | 7.111 | 0.989, 1.186 | 12.566 | 17.479 | 930.1 | 177,719 |
The final warm ranges were 0.989–1.186 seconds before and 0.707–0.866 after; text estimates remain about 61% lower, ~177.7–177.9k to ~69.8–69.9k. Whole-task and cold samples overlap and are retained alongside the original cohort above; these measurements do not establish a general startup or memory advantage.
Follow-up complexity review
Two additional review findings exposed repeated linear work despite bounded retention. The parsed owner now records one agreed residual alias (or explicit conflict) per original type, while preserving every class record and its count. Method identities have a direct residual-signature index; missing signatures still merge into the first record, including the initial unsigned-to-signed transition. Superseded alias arrays and candidate searches were removed. The representation allowance includes the new index containers/entries.
The reproducible
complexity diagnostic
compares the built parser from 160573cb3 with the follow-up parser SHA-256
9c4c24f7a10a17b8ec08e74b2588b9ba87bc30f3e3d16690c3e07c03fbce233b.
The copied before module only rewrites its three unchanged dependency imports
to absolute URLs. Each scenario verifies late matches, counts and answer digest;
answers match across revisions. Node 24.22.1, two CPUs, 512 MiB old space,
8 GiB sampled family guard, one before/after trial per size:
| Scenario | Revision | Parse ms | Lookup ms | Whole s | CPU s | Peak family MiB |
|---|---|---|---|---|---|---|
| 20k identical aliases, 1k candidates × 255 parameters | before | 212.60 | 5169.61 | 5.635 | 5.690 | 149.3 |
| same | after | 207.31 | 22.84 | 0.449 | 0.559 | 117.1 |
| 40k distinct signatures of one original method | before | 8032.93 | 5.70 | 8.300 | 8.371 | 117.5 |
| same | after | 321.58 | 5.43 | 0.562 | 0.532 | 117.8 |
The smaller alias cohort (1k aliases, otherwise identical) looked up in 289.18/24.64 ms before/after. The smaller signature cohort (10k) parsed in 785.65/89.68 ms. These synthetic cases establish removal of the reported growth, not universal latency or memory gains; they involve no agent tokens. All 60 parser, two report and seven public MCP mapping cases pass serially, along with type/lint/build (79.084 s, CPU 110.273 s, peak family 4393.9 MiB).
The final indexed implementation also passes the real source fixture's 43 summary/proof/negative calls (13.838 s, CPU 18.212 s, peak 797.5 MiB), and the Conscrypt 21-call investigation including proof export. The latter's normal-fact digest remains the value above: first task 7.670 s, warm tasks 0.860/0.981 s, whole 12.706 s, CPU 17.870 s, peak 868.8 MiB. Warm whole-task text estimates are 69,940, SDK-inclusive 217,975; tokenizer/scope remain the same. Complete class/method facts and provenance remain accessible.
For CI head 160573cb3, all failed/cancelled shard logs and source-test logs
were audited. Its executed merge ee2398458bd8c56666ecaf120c1c3c21f350eea4
uses main parent 0676ad254c206871e0f4b120ba300cbbf9383b62. The 90 observed
failed test identities were selected on a pristine worktree at that exact
parent: 88 reproduced in the primary run (128.320 s, CPU 147.775 s, peak
2593.3 MiB), then the two newline-bearing ZCode titles reproduced in an exact
follow-up (2.388 s, CPU 2.788 s, peak 416.9 MiB). No feature code was present
in either control. These are baseline failures; cancelled cases without a
completed result remain unverified by CI, with local verification above.
Executed verification and limits
- Parser/reader/protocol/provider mapping regressions cover raw names, exact descriptors/types, overloads, inline outermost frames, malformed/partial records, stable candidate samples, preallocation refusal, repeated metadata, cache freshness/path aliases/deletion, cancellation and descriptor ownership.
- Real R8 source and public-library stdio MCP tasks verify all selected symbols and advertised schemas. Actual CLI complete and summary inspections verify raw/mapped identity, exact normalized parity and complete portable parents.
- Actual Java metadata regression and downstream CLI/MCP body/direct-invoke parity pass, including reserved raw names, frozen aliases, default packages, encoded constants, native absence and classes.dex/classes2.dex identity.
- Summary regressions preserve partial/unknown facts and optional historical omissions, validate SDK output contracts, read retained data after APK/mapping deletion, reject missing portable CLI output and exercise the one-MiB boundary.
- Type/lint/build, named schema/profile checks and relevant source/boundary checks are executed separately. Final PR validation records the current command set.
The final combined 12-file Android run passed 219 of 220 cases, including all 52 session/idle/cleanup integration cases. One existing package-summary SDK case hit its 30-second limit under combined load. The unchanged four-case file then passed with one worker: its SDK case took 11.479 seconds; whole isolated harness 31.867 seconds, CPU 35.064 seconds and peak family 1,551,081,472 bytes. No assertion or timeout was relaxed. The combined timeout remains a recorded resource/concurrency limitation rather than being reported as an all-green run.
Earlier failed attempts are retained rather than counted as passes: a resolver
refactor used only.mapped after only already held the mapped fact; its
regressions failed and were fixed. Tagged-error migration exposed one stale
string-only test. New SDK schema test typing and a redundant JSON preadmission
were corrected at their owning boundaries. A first summary MCP case timed out
while enumerating unrelated full catalog contracts; selecting the actual
Android/application/session families preserves every assertion and passes
without raising timeouts. An initial library harness requested absent
SSL_shutdown and skipped it; the harness now rejects absent requests, and
reported final cohorts explicitly verify all five requested methods.
The mapping cancellation regression distinguishes caller-first from deadline-
first finalization; the original implementation relabelled caller cancellation
as timeout (one failing caller-first case and one passing deadline-first case
on the unchanged optimization control). An initial test's public deadline code was corrected to the actual
provider_timeout contract. Fixed code preserves the first cause and cleanup.
Only Linux x86-64/JDK21/JADX0.7.1 and R8 8.0.40 mapping-format 2.1 were qualified. Residual-signature conflicts use derived negative mappings, not a real mapping- format 2.2 producer. No Windows/macOS, emulator/device, app execution, full APK rebuild/linkage, source/line recovery, APK signature verification or model-usage trial is claimed. Mapping/build association remains explicitly caller-selected.
Reproduction
Generate source/DEX/oracle using the fixture README with selected existing tools. Keep separate before/after worktrees, pin their revisions above, build both and use identical target/mapping/oracle bytes. Then run the same harness for each:
node tests/support/analysis-efficiency/android-mapping.mjs \
--repository "$SELECTED_REPOSITORY" --target "$STATIC_APK" \
--mapping "$MAPPING_FILE" --output "$FRESH_RESULTS" \
--java-home "$SELECTED_JDK" --jar "$JADX_MCP_JAR" \
--oracle "$RAW_DEX_ORACLE" --expectations "$ORIGINAL_EXPECTATIONS" \
--proof
Add --detail summary only on the after implementation. The small fixture
selects all classes/bodies. For Conscrypt add --class-name U0 --repeats 3 and
--methods EVP_PKEY_new_RSA,EVP_DigestSignInit,SSL_new,SSL_free,SSL_get_error;
U0 is the measured artifact's raw class identity, not a universal residual name.
Use the original NativeCrypto D8 descriptor inventory for that expectation file.
Run --variants separately on the source fixture to exercise derived malformed,
partial, duplicate and conflicting mappings; those extra qualification calls
are not part of the timing/token table's repeated task.
python tests/support/analysis-efficiency/android-mapping-costs.py \
--output "$TOKEN_RESULTS" "$BEFORE_RESULTS" "$AFTER_RESULTS"
Capture elapsed/CPU/family RSS with the bounded host runner or an equivalent process-family sampler; alternate trials and keep each label's raw receipts and failed attempts. Complete Evidence stays in the exported per-pass bundles.