From 042b6a0c76cbbee6259f6e7b35abab7b7714ef33 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 17:19:46 +0000 Subject: [PATCH] perf(nip77): use kmp-negentropy v1.1.1 PrefixSumStorageVector for O(1) range fingerprints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kmp-negentropy v1.1.1 adds the `IStorage.fingerprint(begin, end)` seam we needed and ships `PrefixSumStorageVector` — a drop-in IStorage that builds an additive prefix-sum table on seal() and answers any range fingerprint in O(1) instead of re-walking the range. Range fingerprints are the CPU-bound core of a NEG-MSG on a large snapshot; profiling pinned them as the steady-state reconcile cost geode lost multiples on (not serialization). Seal a `PrefixSumStorageVector` in both `NegentropyServerSession.sealVector` (server / relay-relay responder, also backs the `LiveNegentropyIndex` snapshot cache) and `NegentropySession` (initiator). Byte-identical to the plain vector — only the fingerprint path is accelerated. `NegentropyPrefixFingerprintTest` now also asserts the library's `PrefixSumStorageVector.fingerprint` matches the plain walk over 2000 random ranges + boundaries at 50k; the reconcile-shaped mix measures 601× (447 ms → 0.7 ms). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012EZeWww5TJnzBZKPoc6mvU --- gradle/libs.versions.toml | 2 +- .../nip77Negentropy/NegentropyServerSession.kt | 15 +++++++++++---- .../quartz/nip77Negentropy/NegentropySync.kt | 4 ++-- .../NegentropyPrefixFingerprintTest.kt | 18 ++++++++++++++++-- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index adc03d031d..e121e7fa6d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,7 +52,7 @@ material3 = "1.9.0" media3 = "1.10.1" mockk = "1.14.11" kotlinx-coroutines-test = "1.11.0" -negentropyKmp = "v1.0.2" +negentropyKmp = "v1.1.1" netUrlencoderLibVersion = "1.6.0" navigationCompose = "2.9.8" okhttp = "5.4.0" diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip77Negentropy/NegentropyServerSession.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip77Negentropy/NegentropyServerSession.kt index 5879f4b446..a1efa6ab67 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip77Negentropy/NegentropyServerSession.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip77Negentropy/NegentropyServerSession.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip77Negentropy import com.vitorpamplona.negentropy.Negentropy import com.vitorpamplona.negentropy.storage.IStorage -import com.vitorpamplona.negentropy.storage.StorageVector +import com.vitorpamplona.negentropy.storage.PrefixSumStorageVector import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.store.IdAndTime import com.vitorpamplona.quartz.utils.Hex @@ -76,9 +76,16 @@ class NegentropyServerSession( */ const val DEFAULT_FRAME_SIZE_LIMIT: Long = 500_000L - /** Builds and seals a [StorageVector] from `(created_at, id)` pairs. */ - fun sealVector(localEntries: List): StorageVector { - val storage = StorageVector() + /** + * Builds and seals a [PrefixSumStorageVector] from `(created_at, id)` + * pairs. The prefix-sum table (built once on [seal]) answers every + * range fingerprint the reconcile walk needs in O(1) instead of + * O(range), which is the bulk of a NEG-MSG's server-side cost on a + * large snapshot. It's a drop-in [IStorage], byte-identical to the + * plain vector — only the fingerprint path is accelerated. + */ + fun sealVector(localEntries: List): IStorage { + val storage = PrefixSumStorageVector() for (entry in localEntries) { storage.insert(entry.createdAt, entry.id) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip77Negentropy/NegentropySync.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip77Negentropy/NegentropySync.kt index 0e1193d7cc..4427586c3c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip77Negentropy/NegentropySync.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip77Negentropy/NegentropySync.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip77Negentropy import com.vitorpamplona.negentropy.Negentropy -import com.vitorpamplona.negentropy.storage.StorageVector +import com.vitorpamplona.negentropy.storage.PrefixSumStorageVector import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.store.IdAndTime @@ -70,7 +70,7 @@ class NegentropySession( ) } - private val storage = StorageVector() + private val storage = PrefixSumStorageVector() private val negentropy: Negentropy init { diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/NegentropyPrefixFingerprintTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/NegentropyPrefixFingerprintTest.kt index c5918c87d1..703d620724 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/NegentropyPrefixFingerprintTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/NegentropyPrefixFingerprintTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.prodbench import com.vitorpamplona.negentropy.fingerprint.FingerprintCalculator +import com.vitorpamplona.negentropy.storage.PrefixSumStorageVector import com.vitorpamplona.negentropy.storage.StorageVector import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.sha256.sha256 @@ -153,6 +154,13 @@ class NegentropyPrefixFingerprintTest { storage.seal() val library = FingerprintCalculator() + // The library's accelerated storage (now wired into quartz via + // `NegentropyServerSession.sealVector`): same entries, prefix-sum + // table built on seal. Every range must match the plain walk. + val accelerated = PrefixSumStorageVector() + ids.forEachIndexed { i, id -> accelerated.insert(1_700_000_000L + i, id) } + accelerated.seal() + val index = PrefixFingerprintIndex(ids) val rnd = Random(42) @@ -164,10 +172,16 @@ class NegentropyPrefixFingerprintTest { val lo = minOf(a, b) val hi = maxOf(a, b) if (lo == hi) return@repeat + val expected = library.run(storage, lo, hi).bytes assertContentEquals( - library.run(storage, lo, hi).bytes, + expected, index.fingerprint(lo, hi), - "fingerprint mismatch for [$lo,$hi)", + "prototype fingerprint mismatch for [$lo,$hi)", + ) + assertContentEquals( + expected, + accelerated.fingerprint(lo, hi).bytes, + "PrefixSumStorageVector fingerprint mismatch for [$lo,$hi)", ) checked++ }