# Android Performance

## Finding

The Android migration blocker is fixed on the audited Pixel 9 Pro Fold. Three
independent, non-debuggable release
[AndroidX Microbenchmark](https://developer.android.com/topic/performance/benchmarking/microbenchmark-overview)
invocations ran 30 measurements each. The authored SDK facade is faster than
the released Kotlin/Bouncy Castle implementation for every reported median.

Most importantly, the former 64-KiB regression now passes the migration budget:

- encryption is 7.16% faster at the median and 9.11% faster at p95; and
- decryption is 18.54% faster at the median and 9.17% faster at p95.

The gate requires at most 1.25x legacy latency. The audited Pixel therefore
passes with margin, but one additional production Android device is still
required before the two-device gate closes.

## Audit identity

| Field | Value |
| --- | --- |
| Date | 2026-07-26 |
| Device class | Pixel 9 Pro Fold |
| OS | Android API 37, `userdebug` device build |
| Benchmark target | non-debuggable release AndroidX Benchmark |
| Architecture | arm64 |
| SDK benchmark build | `00b9cd857b5ed586fa1fce2efa95c845b47ec7a3` |
| Shared runtime implementation | `472bdc211860d43c1e7ae38578efa90963309ef8` |
| Legacy Android baseline | `a5edacccee3d62757a08dadeceeb3d29a4f4eac6` |
| Independent invocations | 3 |

Changes after the benchmark build through the final audited host revision are
Swift-only, CI/documentation, or preserve the same Android JNI branch. The
minified AAR consumer was rebuilt and rerun after those source changes.

## Released implementation comparison

Values are the median of the three instrumentation-run medians. p95 is the
median of the three run p95 values. Negative deltas mean the SDK is faster.

| Operation | Legacy median ns | SDK median ns | Median delta | Legacy p95 ns | SDK p95 ns | p95 delta |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Import identity | 127,431 | 80,243 | -37.03% | 154,941 | 171,402 | +10.62% |
| NIP-44 encrypt, 32 B | 207,598 | 60,039 | -71.08% | 249,549 | 60,797 | -75.64% |
| NIP-44 decrypt, 32 B | 203,960 | 58,126 | -71.50% | 281,918 | 58,804 | -79.14% |
| NIP-44 encrypt, 1,024 B | 225,127 | 69,380 | -69.18% | 320,048 | 69,916 | -78.15% |
| NIP-44 decrypt, 1,024 B | 225,392 | 67,709 | -69.96% | 289,551 | 68,235 | -76.43% |
| NIP-44 encrypt, 64,000 B | 730,330 | 678,047 | -7.16% | 1,018,122 | 925,322 | -9.11% |
| NIP-44 decrypt, 64,000 B | 741,626 | 604,147 | -18.54% | 987,180 | 896,667 | -9.17% |
| Sign NIP-01 event | 401,046 | 153,195 | -61.80% | 598,553 | 265,061 | -55.72% |
| Parse and verify NIP-01 event | 321,865 | 123,236 | -61.71% | 485,731 | 135,193 | -72.17% |
| Create durable NIP-59 gift wrap | 1,352,071 | 499,986 | -63.02% | 1,627,548 | 536,111 | -67.06% |
| Authenticate and unwrap NIP-59 | 1,139,917 | 378,864 | -66.76% | 1,657,787 | 398,175 | -75.98% |

Identity-import p95 is advisory: its median is substantially faster, its
allocation count falls sharply, and the three-run tail remains sensitive to
device scheduling. It is not part of the 64-KiB migration gate.

## Layer isolation

The 64-KiB measurements identify where time is spent:

| Layer | Encrypt median ns | Decrypt median ns | Purpose |
| --- | ---: | ---: | --- |
| Native Rust | 389,270 | 399,049 | Crypto and Rust allocation floor |
| Generic string FFI round trip | 697,786 | n/a | Echoes a 64-KiB string across UniFFI/JNA |
| Generic string FFI consume/produce | 473,146 consume | 244,052 produce | Isolates one-way conversion cost |
| Authored JNI facade | 678,047 | 604,147 | Public SDK call with byte-array bridge |
| Released Kotlin | 730,330 | 741,626 | Migration baseline |

The earlier regression was boundary cost, not the native cipher. The authored
Android path now sends UTF-8 byte arrays through a narrow JNI bridge and
constructs the Kotlin value once, while generic UniFFI remains the portable JVM
path.

## Allocation result

AndroidX allocation medians also favor the authored facade:

| Operation | Legacy allocations | SDK allocations | Delta |
| --- | ---: | ---: | ---: |
| Import identity | 1,785 | 405 | -77.34% |
| NIP-44 encrypt, 32 B | 2,594 | 7 | -99.73% |
| NIP-44 decrypt, 32 B | 2,581 | 5 | -99.81% |
| NIP-44 encrypt, 64,000 B | 2,594 | 7 | -99.73% |
| NIP-44 decrypt, 64,000 B | 2,582 | 6 | -99.77% |
| Sign NIP-01 event | 5,415 | 350 | -93.53% |
| Parse and verify event | 4,292 | 222 | -94.83% |
| Create gift wrap | 16,246 | 499 | -96.93% |
| Unwrap | 13,870 | 358 | -97.42% |

These are benchmark allocation counts, not heap-retention or PSS
measurements.

## Developer guidance

- Import one `SoftchatIdentity` per signed-in session and retain its cached
  `publicKey`.
- Run crypto, event verification, wrapping, and large parsing away from the
  Android main thread.
- Use the authored `com.softcose.softchat.protocol` API. Do not call the JNI
  bridge or generated UniFFI package directly.
- Pass complete bounded messages. Repeated small FFI calls lose the advantage
  of the coarse facade.
- Keep Room, WorkManager, HTTP, sockets, and file lifecycle in the Android
  layer. Persist a delivery intent before sending it through a relay session.
- Ship the release AAR with its consumer rules and symbols. The verified
  package contains `arm64-v8a` and `x86_64` libraries with 16-KiB page
  alignment and passes an R8-minified external consumer.

## Remaining Android work

- Reproduce median and p95 on one additional production-build device.
- Isolate SDK and legacy PSS, startup, energy, and sustained thermal behavior.
- Calibrate five unchanged-revision runs before turning descriptive numbers
  into automatic device-class gates.
- Exercise Room persistence, process death, migration, quarantine, resume,
  and rollback through an application integration.

Run the complete build, install, three-run collection, and sanitized aggregate
with the commands in [Performance Benchmarks](../../benchmarks/README.md).
