From 292f8a3850c3450c117850a8836538d3eda356f2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 6 Apr 2026 19:22:24 -0400 Subject: [PATCH] Migrates the SHA256 hasher out of the threadpool --- .../EventHasherSerializer.jvmAndroid.kt | 5 +++-- .../crypto/HashingByteArrayBuilder.kt | 11 +++------- .../quartz/utils/sha256/Sha256.jvmAndroid.kt | 21 ++++++++++--------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasherSerializer.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasherSerializer.jvmAndroid.kt index 53b83e0bd..fc98a34b8 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasherSerializer.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasherSerializer.jvmAndroid.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.fastForEach import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper import com.vitorpamplona.quartz.utils.Hex -import com.vitorpamplona.quartz.utils.sha256.pool +import com.vitorpamplona.quartz.utils.sha256.threadLocalDigest import java.io.IOException actual object EventHasherSerializer { @@ -127,7 +127,8 @@ actual object EventHasherSerializer { content: String, ): Boolean { val br: BufferRecycler = JacksonMapper.mapper.factory._getBufferRecycler() - val bb = HashingByteArrayBuilder(br, pool) + val digest = threadLocalDigest.get() + val bb = HashingByteArrayBuilder(br, digest) try { val generator = JacksonMapper.mapper.createGenerator(bb, JsonEncoding.UTF8) generator.enable(JsonGenerator.Feature.COMBINE_UNICODE_SURROGATES_IN_UTF8) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/HashingByteArrayBuilder.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/HashingByteArrayBuilder.kt index 2b832bad1..d6bda4442 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/HashingByteArrayBuilder.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/HashingByteArrayBuilder.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.quartz.nip01Core.crypto import com.fasterxml.jackson.core.util.BufferRecycler -import com.vitorpamplona.quartz.utils.sha256.Sha256Pool import java.io.OutputStream +import java.security.MessageDigest import kotlin.math.max import kotlin.math.min @@ -34,20 +34,15 @@ import kotlin.math.min */ class HashingByteArrayBuilder( private val bufferRecycler: BufferRecycler, - private val hashPool: Sha256Pool, + private val hasher: MessageDigest, ) : OutputStream() { var buf: ByteArray = bufferRecycler.allocByteBuffer(BufferRecycler.BYTE_WRITE_CONCAT_BUFFER) var bufLength: Int = 0 - // lazy minimizes lock conflicts with the pool - val hasher by lazy { - hashPool.acquire() - } - fun release() { bufLength = 0 bufferRecycler.releaseByteBuffer(BufferRecycler.BYTE_WRITE_CONCAT_BUFFER, buf) - hashPool.release(hasher) + hasher.reset() buf = EMPTY_ARRAY } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt index 5ac3608ba..0dc972d8f 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt @@ -30,16 +30,13 @@ import java.security.MessageDigest * (lock acquire + release) for ~2µs of actual hashing. ThreadLocal eliminates all locking * since each thread gets its own MessageDigest instance. digest() implicitly resets state. */ -private val threadLocalDigest = +val threadLocalDigest = ThreadLocal.withInitial { MessageDigest.getInstance("SHA-256") } actual fun sha256(data: ByteArray): ByteArray = threadLocalDigest.get().digest(data) -/** Pool for incremental hashing (used by EventHasherSerializer, HashingByteArrayBuilder). */ -val pool = Sha256Pool(25) - /** * Calculate SHA256 hash while counting bytes read from the stream. * Returns both the hash and the number of bytes processed. @@ -55,11 +52,15 @@ fun sha256StreamWithCount( ): Pair { val countingStream = CountingInputStream(inputStream) val digest = threadLocalDigest.get() - val buffer = ByteArray(bufferSize) - var bytesRead: Int - while (countingStream.read(buffer).also { bytesRead = it } != -1) { - digest.update(buffer, 0, bytesRead) + try { + val buffer = ByteArray(bufferSize) + var bytesRead: Int + while (countingStream.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + return Pair(digest.digest(), countingStream.bytesRead) + } catch (e: Exception) { + digest.reset() // Clean up state on failure + throw e } - val hash = digest.digest() - return Pair(hash, countingStream.bytesRead) }