11import { createTreecrdtClient , type TreecrdtClient } from '@treecrdt/wa-sqlite' ;
22import { makeOp , nodeIdFromInt , type BenchmarkResult } from '@treecrdt/benchmark' ;
33import type { Operation } from '@treecrdt/interface' ;
4+ import { bytesToHex } from '@treecrdt/interface/ids' ;
5+ import { SyncPeer , type SyncBackend } from '@treecrdt/sync-protocol' ;
6+ import { treecrdtSyncV0ProtobufCodec } from '@treecrdt/sync-protocol/protobuf' ;
7+ import {
8+ createInMemoryDuplex ,
9+ wrapDuplexTransportWithCodec ,
10+ } from '@treecrdt/sync-protocol/transport' ;
411import { replicaFromLabel } from './op-helpers.js' ;
512
613type RuntimeChoice = 'direct' | 'dedicated-worker' | 'shared-worker' ;
714type StorageChoice = 'memory' | 'opfs' ;
15+ type RemoteIngestChoice = 'append-many' | 'sync-peer' ;
816
917type RuntimeMixedWriteBenchOptions = {
1018 runtime : RuntimeChoice ;
1119 storage ?: StorageChoice ;
20+ remoteIngest ?: RemoteIngestChoice ;
1221 docId ?: string ;
1322 filename ?: string ;
1423 prefillOps ?: number ;
@@ -25,6 +34,7 @@ type RuntimeMixedWriteBenchResult = BenchmarkResult & {
2534 extra : {
2635 runtime : RuntimeChoice ;
2736 storage : StorageChoice ;
37+ remoteIngest : RemoteIngestChoice ;
2838 prefillOps : number ;
2939 remoteOps : number ;
3040 remoteBatchSize : number ;
@@ -46,6 +56,7 @@ type RuntimeMixedWriteBenchResult = BenchmarkResult & {
4656 readP50Ms : number ;
4757 readP95Ms : number ;
4858 readMaxMs : number ;
59+ interBatchReadMs : number | null ;
4960 localWriteIntervalMs : number ;
5061 readIntervalMs : number ;
5162 yieldBetweenRemoteBatchesMs : number ;
@@ -96,6 +107,16 @@ function summarizeDurations(durationsMs: number[]) {
96107 } ;
97108}
98109
110+ function deferredPromise < T = void > ( ) {
111+ let resolve ! : ( value : T | PromiseLike < T > ) => void ;
112+ let reject ! : ( reason ?: unknown ) => void ;
113+ const promise = new Promise < T > ( ( res , rej ) => {
114+ resolve = res ;
115+ reject = rej ;
116+ } ) ;
117+ return { promise, resolve, reject } ;
118+ }
119+
99120function makeInsertOps ( opts : {
100121 replica : Uint8Array ;
101122 count : number ;
@@ -122,6 +143,7 @@ export async function runRuntimeMixedWriteBench(
122143) : Promise < RuntimeMixedWriteBenchResult > {
123144 const runtime = opts . runtime ;
124145 const storage = opts . storage ?? 'opfs' ;
146+ const remoteIngest = opts . remoteIngest ?? 'sync-peer' ;
125147 const prefillOps = opts . prefillOps ?? 0 ;
126148 const remoteOps = opts . remoteOps ?? 2_000 ;
127149 const remoteBatchSize = opts . remoteBatchSize ?? 500 ;
@@ -164,10 +186,12 @@ export async function runRuntimeMixedWriteBench(
164186 const remoteBatchDurationsMs : number [ ] = [ ] ;
165187 const localWriteDurationsMs : number [ ] = [ ] ;
166188 const readDurationsMs : number [ ] = [ ] ;
189+ let interBatchReadMs : number | null = null ;
190+ let interBatchReadDone : Promise < void > | null = null ;
167191 let finalChildCount = 0 ;
168192 const start = performance . now ( ) ;
169193
170- const runRemoteIngest = async ( ) => {
194+ const runAppendManyRemoteIngest = async ( ) => {
171195 for ( let batchIndex = 0 ; batchIndex < remoteBatchCount ; batchIndex += 1 ) {
172196 const batchStartCounter = batchIndex * remoteBatchSize + 1 ;
173197 const remaining = remoteOps - batchIndex * remoteBatchSize ;
@@ -195,6 +219,90 @@ export async function runRuntimeMixedWriteBench(
195219 }
196220 } ;
197221
222+ const runSyncPeerRemoteIngest = async ( ) => {
223+ if ( remoteOps === 0 ) return ;
224+
225+ const remoteApplied = deferredPromise ( ) ;
226+ let appliedRemoteOps = 0 ;
227+ const receiverBackend : SyncBackend < Operation > = {
228+ docId,
229+ maxLamport : async ( ) => BigInt ( await client . meta . headLamport ( ) ) ,
230+ listOpRefs : async ( filter ) => {
231+ if ( 'all' in filter ) return client . opRefs . all ( ) ;
232+ return client . opRefs . children ( bytesToHex ( filter . children . parent ) ) ;
233+ } ,
234+ getOpsByOpRefs : async ( opRefs ) => client . ops . get ( opRefs ) ,
235+ applyOps : async ( ops ) => {
236+ if ( ops . length === 0 ) return ;
237+
238+ const batchStart = performance . now ( ) ;
239+ try {
240+ await client . ops . appendMany ( ops ) ;
241+ } catch ( error ) {
242+ remoteApplied . reject ( error ) ;
243+ throw error ;
244+ }
245+
246+ remoteBatchDurationsMs . push ( performance . now ( ) - batchStart ) ;
247+ const nextAppliedRemoteOps = appliedRemoteOps + ops . length ;
248+ if ( appliedRemoteOps === 0 && nextAppliedRemoteOps < remoteOps ) {
249+ interBatchReadDone = new Promise < void > ( ( resolve , reject ) => {
250+ setTimeout ( ( ) => {
251+ const readStart = performance . now ( ) ;
252+ client . tree . children ( rootNode ( ) ) . then ( ( ) => {
253+ interBatchReadMs = performance . now ( ) - readStart ;
254+ resolve ( ) ;
255+ } , reject ) ;
256+ } , 0 ) ;
257+ } ) ;
258+ }
259+ appliedRemoteOps = nextAppliedRemoteOps ;
260+ if ( appliedRemoteOps >= remoteOps ) remoteApplied . resolve ( ) ;
261+ } ,
262+ } ;
263+ const senderBackend : SyncBackend < Operation > = {
264+ docId,
265+ maxLamport : async ( ) => 0n ,
266+ listOpRefs : async ( ) => [ ] ,
267+ getOpsByOpRefs : async ( ) => [ ] ,
268+ applyOps : async ( ) => { } ,
269+ } ;
270+ const [ wireA , wireB ] = createInMemoryDuplex < Uint8Array > ( ) ;
271+ const transportA = wrapDuplexTransportWithCodec ( wireA , treecrdtSyncV0ProtobufCodec ) ;
272+ const transportB = wrapDuplexTransportWithCodec ( wireB , treecrdtSyncV0ProtobufCodec ) ;
273+ const senderPeer = new SyncPeer ( senderBackend , { maxOpsPerBatch : remoteBatchSize } ) ;
274+ const receiverPeer = new SyncPeer ( receiverBackend , { maxOpsPerBatch : remoteBatchSize } ) ;
275+ const detachSender = senderPeer . attach ( transportA ) ;
276+ const detachReceiver = receiverPeer . attach ( transportB ) ;
277+
278+ try {
279+ const ops = makeInsertOps ( {
280+ replica : remoteReplica ,
281+ count : remoteOps ,
282+ startCounter : 1 ,
283+ startLamport : prefillOps + 1 ,
284+ startNodeInt : 100_000 ,
285+ startOrderOffset : 100_000 ,
286+ } ) ;
287+ await senderPeer . pushOps ( transportA , ops , {
288+ filterId : `runtime-sync-peer-${ crypto . randomUUID ( ) } ` ,
289+ maxOpsPerBatch : remoteBatchSize ,
290+ } ) ;
291+ await remoteApplied . promise ;
292+ if ( interBatchReadDone ) await interBatchReadDone ;
293+ } catch ( error ) {
294+ throw new Error (
295+ `runtime mixed benchmark remote sync ingest failed: ${ errorMessage ( error ) } ` ,
296+ ) ;
297+ } finally {
298+ detachSender ( ) ;
299+ detachReceiver ( ) ;
300+ }
301+ } ;
302+
303+ const runRemoteIngest =
304+ remoteIngest === 'sync-peer' ? runSyncPeerRemoteIngest : runAppendManyRemoteIngest ;
305+
198306 const runLocalWrites = async ( ) => {
199307 for ( let i = 0 ; i < localWrites ; i += 1 ) {
200308 if ( i > 0 && localWriteIntervalMs > 0 ) await sleep ( localWriteIntervalMs ) ;
@@ -242,13 +350,14 @@ export async function runRuntimeMixedWriteBench(
242350 const readSummary = summarizeDurations ( readDurationsMs ) ;
243351
244352 return {
245- name : `runtime-mixed-sync -ingest-local-writes-${ storage } -${ runtime } -prefill-${ prefillOps } ` ,
353+ name : `runtime-mixed-${ remoteIngest } -ingest-local-writes-${ storage } -${ runtime } -prefill-${ prefillOps } ` ,
246354 totalOps,
247355 durationMs,
248356 opsPerSec : durationMs > 0 ? ( totalOps / durationMs ) * 1000 : Infinity ,
249357 extra : {
250358 runtime,
251359 storage,
360+ remoteIngest,
252361 prefillOps,
253362 remoteOps,
254363 remoteBatchSize,
@@ -270,6 +379,7 @@ export async function runRuntimeMixedWriteBench(
270379 readP50Ms : readSummary . p50Ms ,
271380 readP95Ms : readSummary . p95Ms ,
272381 readMaxMs : readSummary . maxMs ,
382+ interBatchReadMs,
273383 localWriteIntervalMs,
274384 readIntervalMs,
275385 yieldBetweenRemoteBatchesMs,
0 commit comments