Skip to main content

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 replayBaseline median msCurrent median msBaseline p95 msCurrent p95 msCurrent median allocations
No logging sink1.1941.1621.9961.869385.02
INFO sink1.2441.1632.2632.139385.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

FieldValue
Date2026-08-04
Device classPixel 9 Pro Fold
OSAndroid API 37, userdebug device build
Benchmark targetnon-debuggable release AndroidX Benchmark
Architecturearm64
Semantic AAR sourcebb3cfc8ddc5157f4a54baa751db7e056531583a1
Benchmark harnessbb3cfc8ddc5157f4a54baa751db7e056531583a1
Legacy Android baselinea5edacccee3d62757a08dadeceeb3d29a4f4eac6
Independent invocations3; 30 measurements for legacy pairs and 50 for account queries
Configurationcompilation 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

OperationLegacy median nsSDK median nsMedian deltaLegacy p95 nsSDK p95 nsp95 delta
Import identity126,85784,297-33.55%205,332174,038-15.24%
NIP-44 encrypt, 32 B211,06559,337-71.89%339,04660,065-82.28%
NIP-44 decrypt, 32 B208,12558,077-72.10%249,43458,527-76.54%
NIP-44 encrypt, 1,024 B226,42766,810-70.49%361,00867,353-81.34%
NIP-44 decrypt, 1,024 B224,13665,716-70.68%307,93466,168-78.51%
NIP-44 encrypt, 64,000 B725,921537,662-25.93%995,685634,962-36.23%
NIP-44 decrypt, 64,000 B746,356502,779-32.64%1,002,308697,535-30.41%
Sign NIP-01 event406,082174,244-57.09%511,626184,932-63.85%
Parse and verify NIP-01 event326,600135,162-58.62%445,733148,118-66.77%
Create durable NIP-59 gift wrap1,374,173527,899-61.58%1,846,003568,026-69.23%
Authenticate and unwrap NIP-591,135,718366,369-67.74%1,861,038385,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:

LayerEncrypt median nsDecrypt median nsPurpose
Native Rust395,420392,405Crypto and Rust allocation floor
Generated UniFFI/JNA binding1,224,8711,274,731Internal portable binding, not the Android product API
Generic string FFI round trip716,730n/aEchoes a 64-KiB string across UniFFI/JNA
Generic one-way FFI485,481 consume234,742 produceIsolates conversion direction
Authored Android facade537,662502,779Public SDK call with the narrow JNI bridge
Released Kotlin725,921746,356Adjacent 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.

OperationMedianp95Calls/sResult throughputAllocations/call
Open existing account file1.000 ms1.077 ms1,000.0n/a975.3
50 complete conversation rows4.196 ms7.771 ms238.311,916 rows/s2,322.9
50 complete message rows2.958 ms4.356 ms338.016,902 rows/s1,504.4
50 effective profiles2.234 ms3.371 ms447.622,380 profiles/s496.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.

ConfigurationMedianp95Calls/sAllocations/call
Logging disabled1.706 ms3.442 ms586.1386.7
INFO sink2.009 ms3.362 ms497.7389.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:

OperationLegacy allocationsSDK allocationsDelta
Import identity1,785.0378.5-78.79%
NIP-44 encrypt, 32 B2,594.06.0-99.77%
NIP-44 decrypt, 32 B2,581.04.0-99.85%
NIP-44 encrypt, 64,000 B2,594.06.0-99.77%
NIP-44 decrypt, 64,000 B2,582.05.0-99.81%
Sign NIP-01 event5,415.0269.2-95.03%
Parse and verify event4,292.0294.7-93.13%
Create gift wrap16,248.6499.2-96.93%
Unwrap13,834.2358.1-97.41%

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.sdk 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.
  • Open one SoftchatAccount per 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-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.
  • 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.