Android Performance
Account diagnostics comparison (2026-09-12)
A focused regression check compares SDK 6a9065d with d9f9c26 on the same
Pixel 9 Pro Fold, API 37 userdebug. Three independent invocations of each
non-debuggable release benchmark alternate baseline/current, with 50 measurements
per case, compilation mode speed, and profiling disabled. The benchmark is
not minified. Both builds enable managed-transport, as required by the current
Android facade; the benchmark build command now includes that feature.
| Warm idempotent follow replay | Baseline median ms | Current median ms | Baseline p95 ms | Current p95 ms | Current median allocations |
|---|---|---|---|---|---|
| No logging sink | 1.194 | 1.162 | 1.996 | 1.869 | 385.02 |
| INFO sink | 1.244 | 1.163 | 2.263 | 2.139 | 385.03 |
Values are medians of the three run medians/p95 values. No regression was observed in these workloads; the small latency differences remain descriptive. The command uses a fixed idempotency key after warmup, so this measures the facade and replay policy. New diagnostics correctly suppress INFO for that replay. It does not measure a new encrypted publication, idle polling energy, or minified application throughput. Those limits prevent interpreting this as a general SDK speedup.
The comparison and
audit manifest preserve
sanitized results, exact revisions, order, and hashes of all six raw exports.
Raw exports remain under target/performance/account-diagnostics-2026-09-12.
This supplementary check leaves the broader August audit and its accepted
performance cards below intact.
Finding
The Android application migration candidate and Rust-owned account database
pass on the audited Pixel 9 Pro Fold at SDK revision bb3cfc8. Three
independent, non-debuggable release
AndroidX Microbenchmark
invocations measured the exact packaged boundary used by the application
migration. Legacy comparison operations ran 30 measurements per invocation;
account and logging operations ran 50. The authored SDK facade is faster than
the released Kotlin/Bouncy Castle implementation for every matched median and
p95 measurement.
The 64-KiB migration result has substantial margin:
- encryption is 25.93% faster at the median and 36.23% faster at p95; and
- decryption is 32.64% faster at the median and 30.41% 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-08-04 |
| Device class | Pixel 9 Pro Fold |
| OS | Android API 37, userdebug device build |
| Benchmark target | non-debuggable release AndroidX Benchmark |
| Architecture | arm64 |
| Semantic AAR source | bb3cfc8ddc5157f4a54baa751db7e056531583a1 |
| Benchmark harness | bb3cfc8ddc5157f4a54baa751db7e056531583a1 |
| Legacy Android baseline | a5edacccee3d62757a08dadeceeb3d29a4f4eac6 |
| Independent invocations | 3; 30 measurements for legacy pairs and 50 for account queries |
| Configuration | compilation mode speed, profiling disabled |
Legacy and SDK measurements ran as deterministic adjacent pairs to limit ordering and thermal bias. Values below are the median of the three run medians; p95 is the median of the three run p95 values. Negative deltas mean the SDK is faster.
Released implementation comparison
| Operation | Legacy median ns | SDK median ns | Median delta | Legacy p95 ns | SDK p95 ns | p95 delta |
|---|---|---|---|---|---|---|
| Import identity | 126,857 | 84,297 | -33.55% | 205,332 | 174,038 | -15.24% |
| NIP-44 encrypt, 32 B | 211,065 | 59,337 | -71.89% | 339,046 | 60,065 | -82.28% |
| NIP-44 decrypt, 32 B | 208,125 | 58,077 | -72.10% | 249,434 | 58,527 | -76.54% |
| NIP-44 encrypt, 1,024 B | 226,427 | 66,810 | -70.49% | 361,008 | 67,353 | -81.34% |
| NIP-44 decrypt, 1,024 B | 224,136 | 65,716 | -70.68% | 307,934 | 66,168 | -78.51% |
| NIP-44 encrypt, 64,000 B | 725,921 | 537,662 | -25.93% | 995,685 | 634,962 | -36.23% |
| NIP-44 decrypt, 64,000 B | 746,356 | 502,779 | -32.64% | 1,002,308 | 697,535 | -30.41% |
| Sign NIP-01 event | 406,082 | 174,244 | -57.09% | 511,626 | 184,932 | -63.85% |
| Parse and verify NIP-01 event | 326,600 | 135,162 | -58.62% | 445,733 | 148,118 | -66.77% |
| Create durable NIP-59 gift wrap | 1,374,173 | 527,899 | -61.58% | 1,846,003 | 568,026 | -69.23% |
| Authenticate and unwrap NIP-59 | 1,135,718 | 366,369 | -67.74% | 1,861,038 | 385,063 | -79.31% |
Every SDK median and p95 is faster than its paired legacy measurement. Attachment streaming, which has no legacy equivalent, measured 971,894 ns at the median, 1,144,657 ns at p95, and 598.3 allocations for 64,000 bytes.
AndroidX Benchmark inserted thermal recovery pauses when required. Every measurement completed; the values remain descriptive until unchanged-revision and production-device calibration is complete.
Layer isolation
The 64-KiB measurements identify where time is spent:
| Layer | Encrypt median ns | Decrypt median ns | Purpose |
|---|---|---|---|
| Native Rust | 395,420 | 392,405 | Crypto and Rust allocation floor |
| Generated UniFFI/JNA binding | 1,224,871 | 1,274,731 | Internal portable binding, not the Android product API |
| Generic string FFI round trip | 716,730 | n/a | Echoes a 64-KiB string across UniFFI/JNA |
| Generic one-way FFI | 485,481 consume | 234,742 produce | Isolates conversion direction |
| Authored Android facade | 537,662 | 502,779 | Public SDK call with the narrow JNI bridge |
| Released Kotlin | 725,921 | 746,356 | Adjacent 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. Absolute values moved with device state between audits, which is why the decision uses paired SDK/legacy results from the same invocation.
Rust-owned account SQLite
The Android migration now performs protocol persistence and complete use-case
queries through SoftchatAccount. The benchmark invokes the exact public
suspending APIs used by the app—conversations.page, messages.page, and
profile.page—including coroutine dispatch, JNI, Rust mapping, bounded SQL,
and Kotlin value conversion. These are current-only measurements because the
released Room model did not retain equivalent authenticated truth and cannot
provide a valid operation-for-operation baseline.
| Operation | Median | p95 | Calls/s | Result throughput | Allocations/call |
|---|---|---|---|---|---|
| Open existing account file | 1.000 ms | 1.077 ms | 1,000.0 | n/a | 975.3 |
| 50 complete conversation rows | 4.196 ms | 7.771 ms | 238.3 | 11,916 rows/s | 2,322.9 |
| 50 complete message rows | 2.958 ms | 4.356 ms | 338.0 | 16,902 rows/s | 1,504.4 |
| 50 effective profiles | 2.234 ms | 3.371 ms | 447.6 | 22,380 profiles/s | 496.8 |
Values are the median of three independent run medians; p95 is the median of three run p95 values. The reopen workload is a warm synthetic file open, not an Android cold-start claim.
A pre-batch diagnostic checkpoint exposed N+1 native queries behind all three public pages. Set-based conversation aggregation, batched effective-message loading, batched profile resolution, and covering projection indices reduced their approximate medians from 43.1, 26.1, and 11.5 ms to the accepted 3.699, 1.967, and 1.316 ms respectively. That is a 91.42%, 92.46%, and 88.56% reduction between unreleased Rust SQLite implementations; it is not a comparison with the old Room library. The refreshed query series had high within-run timing variation while allocations stayed stable, so the absolute latencies remain descriptive rather than a calibrated regression gate.
Structured diagnostics overhead
The migration logger is measured around the same complete atomic follow
operation with collection disabled and with an INFO sink configured. The
measurement includes native timing, ring insertion and aggregation, the
authored Android boundary, and final asynchronous drain on account close. It
does not synchronously call application code from the Rust operation.
| Configuration | Median | p95 | Calls/s | Allocations/call |
|---|---|---|---|---|
| Logging disabled | 1.706 ms | 3.442 ms | 586.1 | 386.7 |
INFO sink | 2.009 ms | 3.362 ms | 497.7 | 389.6 |
INFO collection adds 0.303 ms, or 17.76%, at the median and 0.74% allocation
overhead. Its p95 is 2.32% lower in this sample. Both timing series have high
variation, so the useful conclusions are the bounded absolute cost and stable
allocation count, not the direction of the p95 difference. The disabled path
remains one relaxed atomic threshold read and creates no logging coroutine.
Allocation result
AndroidX allocation medians also favor the authored facade:
| Operation | Legacy allocations | SDK allocations | Delta |
|---|---|---|---|
| Import identity | 1,785.0 | 378.5 | -78.79% |
| NIP-44 encrypt, 32 B | 2,594.0 | 6.0 | -99.77% |
| NIP-44 decrypt, 32 B | 2,581.0 | 4.0 | -99.85% |
| NIP-44 encrypt, 64,000 B | 2,594.0 | 6.0 | -99.77% |
| NIP-44 decrypt, 64,000 B | 2,582.0 | 5.0 | -99.81% |
| Sign NIP-01 event | 5,415.0 | 269.2 | -95.03% |
| Parse and verify event | 4,292.0 | 294.7 | -93.13% |
| Create gift wrap | 16,248.6 | 499.2 | -96.93% |
| Unwrap | 13,834.2 | 358.1 | -97.41% |
These are benchmark allocation counts, not heap-retention or PSS measurements.
Developer guidance
- Import one
SoftchatIdentityper signed-in session and retain its cachedpublicKey. - Run crypto, event verification, wrapping, and large parsing away from the Android main thread.
- Use the authored
com.softcose.softchat.sdkAPI. 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.
- Open one
SoftchatAccountper signed-in authority and close it on account removal or switch. Multiple accounts may coexist when the product supports concurrent authorities. - Call the public suspending account methods from a lifecycle-owned coroutine.
They dispatch bounded native reads and writes to
Dispatchers.IO; do not add another database executor or query Rust SQLite directly. - Let Rust own the account SQLite file, schema, transactions, projections, delivery intent, retry, and recovery. Do not open that file through Room or execute application SQL against it.
- Keep WorkManager, HTTP, sockets, file lifecycle, and Compose in Android. Claim only committed delivery work before sending it through a relay.
- Ship the release AAR with its consumer rules and symbols. The verified
package contains
arm64-v8aandx86_64libraries 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.
- Verify fresh install, account import, process-death recovery, and production-scale history on the migrated application.
- Repeat the account-query and logging series on the calibrated second device before assigning automatic regression thresholds.
Run the complete build, install, three-run collection, and sanitized aggregate with the commands in the Performance Benchmarks source guide.