mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
fix(cashu): warmup must be at-most-once per process
The previous warmup change introduced a startup crash: each account's
CashuWalletState.start() spawned a coroutine on Dispatchers.Default
that ran Bdhke.warmup() (32 blind+unblind cycles) AND
MintApiSerializerWarmup.warmup() (decode 32-element synthetic
RestoreResponseDto). With two accounts, that's 128 BDHKE calls + 64
deserializations all in flight at once — far worse JIT pressure than
the original problem, since the warmup explicitly tries to make
methods hot.
The trace showed it clearly — multiple unblind/blind logs from
different threads interleaving mid-call ("unblind: parseAffinePointInto k"
appearing without a preceding "unblind: parseAffinePointInto cTick"
from the same logical call). ART's optimizer then crashed on the
flood.
Add a @Volatile flag to both warmups. First caller does the work;
every subsequent caller returns immediately. Plain volatile (not
atomic CAS or synchronized) because:
- The race window is tiny (microseconds between read and write)
- Two extra 32-cycle warmups in the worst case isn't a correctness
or performance issue
- Stays commonMain-portable without expect/actual or atomicfu
This commit is contained in:
@@ -621,6 +621,27 @@ object Bdhke {
|
||||
*/
|
||||
fun randomSecret(): ByteArray = randomScalar()
|
||||
|
||||
/**
|
||||
* Tracks whether [warmup] has already been invoked this process.
|
||||
* At-most-once: the FIRST caller does the JIT-warming work; every
|
||||
* subsequent caller (including parallel ones from different
|
||||
* accounts' [com.vitorpamplona.amethyst.model.nip60Cashu.CashuWalletState.start])
|
||||
* sees the flag and returns immediately.
|
||||
*
|
||||
* Multiple warmups running concurrently was itself a crash trigger
|
||||
* — two accounts each doing 32 blind+unblind cycles on
|
||||
* Dispatchers.Default at startup put ~128 concurrent BDHKE calls
|
||||
* in flight, contending for the JIT compiler and reproducing the
|
||||
* Android 15+ ART optimizer bug we were trying to dodge.
|
||||
*
|
||||
* Plain @Volatile + check is sufficient — the harm from a tiny
|
||||
* race window (two callers both seeing `false` before either
|
||||
* flips the flag) is one extra 32-cycle warmup, not a correctness
|
||||
* issue. We avoid `synchronized` to stay commonMain-portable.
|
||||
*/
|
||||
@Volatile
|
||||
private var warmupDone: Boolean = false
|
||||
|
||||
/**
|
||||
* Pre-warm the JIT for [blind] / [unblind] by running them N times
|
||||
* with synthetic data. Forces ART's optimizing compiler to do its
|
||||
@@ -630,13 +651,14 @@ object Bdhke {
|
||||
* the whole process down.
|
||||
*
|
||||
* Called from `CashuWalletState.start()` on a background coroutine.
|
||||
* Safe to call multiple times — the JIT will deduplicate. Cheap if
|
||||
* the methods are already compiled.
|
||||
* At-most-once per process — see [warmupDone].
|
||||
*
|
||||
* The synthetic data uses a fixed mint pubkey / random blinding
|
||||
* factors. It does NOT touch any wallet state or network.
|
||||
*/
|
||||
fun warmup() {
|
||||
if (warmupDone) return
|
||||
warmupDone = true
|
||||
val scratch = BdhkeScratchpad()
|
||||
// Fixed public key for warmup — generator point G's compressed form.
|
||||
// G is always on the curve and parses cleanly; nothing we do
|
||||
|
||||
+12
@@ -64,7 +64,19 @@ object MintApiSerializerWarmup {
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
/**
|
||||
* At-most-once flag, mirrors [com.vitorpamplona.quartz.nip60Cashu.bdhke.Bdhke.warmup]'s
|
||||
* gate. Multiple accounts' `CashuWalletState.start()` racing to
|
||||
* warm the serializer simultaneously defeats the purpose — it
|
||||
* stacks N parallel decodes into the JIT's queue at exactly the
|
||||
* moment we wanted things calm.
|
||||
*/
|
||||
@Volatile
|
||||
private var warmupDone: Boolean = false
|
||||
|
||||
fun warmup() {
|
||||
if (warmupDone) return
|
||||
warmupDone = true
|
||||
val payload = buildSyntheticRestorePayload(WARMUP_ELEMENT_COUNT)
|
||||
// Decode + re-encode. The encode path is also hot (every mint
|
||||
// request body serializes a List<BlindedMessageDto>), so warm
|
||||
|
||||
Reference in New Issue
Block a user