Skip to main content

Kotlin/JVM Performance

Finding

The Kotlin/JVM facade has clear structural gains but the plain-loop crypto latency remains advisory. Identity import is 14.73% faster, event parsing and signing are faster, cached public-key access removes a JNA call, and 64-KiB crypto is neutral. The 32-byte and 1-KiB encryption measurements, plus 32-byte decryption, remain slower and have wide GC/JIT-sensitive tails.

This does not reproduce in native Rust or on the production Android JNI path. Both direct-payload and aggregate-record JVM crossings were tested during the optimization pass; their small-payload ranking moved between hosted runs. Do not set a JVM release threshold from this loop. Replace it with forked JMH measurements and allocation profiling first.

The final audit used three paired JVM processes on a hosted Ubuntu 24.04 x86_64 runner with Temurin Java 17.0.19. Values are nanoseconds per authored Kotlin call. Negative deltas mean the current SDK is faster.

OperationBytesOld median nsCurrent median nsDeltaCurrent p95 nsp95 delta
Import identity32198,689169,432-14.73%384,613-9.78%
Read cached public key0101,1815-100.00%5-100.00%
NIP-44 encrypt32211,476274,097+29.61%538,004+6.85%
NIP-44 decrypt32142,170208,493+46.65%380,252+27.92%
NIP-44 encrypt1,024112,725171,717+52.33%300,648+27.02%
NIP-44 decrypt1,024101,67899,590-2.05%257,585+33.47%
NIP-44 encrypt64,000452,967445,990-1.54%478,146-27.77%
NIP-44 decrypt64,000508,228510,611+0.47%559,425+2.61%
Sign NIP-01 event32212,960204,906-3.78%259,817-4.77%
Parse and verify event37486,60484,743-2.15%131,131+7.82%

The 5-ns getter is an API-shape check rather than an absolute latency claim.

Current-only baseline

OperationBytesMedian nsp95 ns
Create NIP-59 rumor15153,025214,991
Create NIP-17 text message15235,957631,869
Create durable gift wrap15963,7341,412,039
Authenticate and unwrap15285,722432,626
Stream-encrypt attachment64,000502,816851,059

Optimizations retained

  • SoftchatIdentity.import returns the native handle and immutable public metadata in one call; publicKey is then a Kotlin-owned value.
  • The Android JNI registry is absent from plain JVM native builds.
  • JVM encryption below 4,096 characters and decryption below an 8,192-character encoded payload use the aggregate crossing; larger payloads avoid the extra aggregate copy. These thresholds are implementation details, not API limits.
  • Android does not use either JVM branch: it selects the measured JNI byte-array path once an identity is imported.
  • All public event, envelope, relay, and media values stay in the authored package; generated JNA records remain internal.

Footprint

Artifact setOld bytesCurrent bytesDelta
Authored and generated JARs209,822773,170+268.49%

JAR growth reflects the complete generated protocol binding and authored facade. The native library ships separately; see Rust performance. JAR size is not JVM RSS, and the current loop does not produce a trustworthy binding-specific memory number.

Developer guidance

  • Import one identity per signed-in session and retain its cached publicKey.
  • Run signing, encryption, parsing, wrapping, reconciliation, and attachment work on a bounded CPU dispatcher, not a latency-sensitive coroutine or the Android main thread.
  • Pass complete bounded values across the facade. Avoid repeated generated calls for fields or tags.
  • Use com.softcose.softchat.protocol; never couple application code to com.softcose.softchat.internal.ffi or the Android JNI bridge.
  • Keep database transactions, socket lifecycle, WorkManager, HTTP, and files in platform code. Persist delivery intent before asking the relay session to send.
  • Call erase() only when identity authority must end early; normal ownership follows the authenticated session.

Use the physical Android report for Android migration decisions. Use the Performance Benchmarks source guide for the current JVM loop, but treat it as diagnostic only.

Open work

Add a JMH module with multiple forks, warm-up and measurement iterations, Blackhole, allocation profiling, and a documented GC. Only then decide whether the remaining small-payload signal belongs to JNA dispatch, record lifting, allocation/GC, or the harness itself. No cryptographic cache or secret-derived optimization is justified by the current data.