# 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.

| Operation | Bytes | Old median ns | Current median ns | Delta | Current p95 ns | p95 delta |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Import identity | 32 | 198,689 | 169,432 | -14.73% | 384,613 | -9.78% |
| Read cached public key | 0 | 101,181 | 5 | -100.00% | 5 | -100.00% |
| NIP-44 encrypt | 32 | 211,476 | 274,097 | +29.61% | 538,004 | +6.85% |
| NIP-44 decrypt | 32 | 142,170 | 208,493 | +46.65% | 380,252 | +27.92% |
| NIP-44 encrypt | 1,024 | 112,725 | 171,717 | +52.33% | 300,648 | +27.02% |
| NIP-44 decrypt | 1,024 | 101,678 | 99,590 | -2.05% | 257,585 | +33.47% |
| NIP-44 encrypt | 64,000 | 452,967 | 445,990 | -1.54% | 478,146 | -27.77% |
| NIP-44 decrypt | 64,000 | 508,228 | 510,611 | +0.47% | 559,425 | +2.61% |
| Sign NIP-01 event | 32 | 212,960 | 204,906 | -3.78% | 259,817 | -4.77% |
| Parse and verify event | 374 | 86,604 | 84,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

| Operation | Bytes | Median ns | p95 ns |
| --- | ---: | ---: | ---: |
| Create NIP-59 rumor | 15 | 153,025 | 214,991 |
| Create NIP-17 text message | 15 | 235,957 | 631,869 |
| Create durable gift wrap | 15 | 963,734 | 1,412,039 |
| Authenticate and unwrap | 15 | 285,722 | 432,626 |
| Stream-encrypt attachment | 64,000 | 502,816 | 851,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 set | Old bytes | Current bytes | Delta |
| --- | ---: | ---: | ---: |
| Authored and generated JARs | 209,822 | 773,170 | +268.49% |

JAR growth reflects the complete generated Phase 2 binding and authored
facade. The native library ships separately; see [Rust performance](rust.md).
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](android.md) for Android migration decisions.
Use [Performance Benchmarks](../../benchmarks/README.md) 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.
