@peerbit/shared-log 13.1.15 → 13.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/benchmark/native-graph.d.ts +2 -0
  2. package/dist/benchmark/native-graph.d.ts.map +1 -0
  3. package/dist/benchmark/native-graph.js +249 -0
  4. package/dist/benchmark/native-graph.js.map +1 -0
  5. package/dist/benchmark/sync-batch-sweep.js +72 -24
  6. package/dist/benchmark/sync-batch-sweep.js.map +1 -1
  7. package/dist/benchmark/sync-phase-profile.d.ts +2 -0
  8. package/dist/benchmark/sync-phase-profile.d.ts.map +1 -0
  9. package/dist/benchmark/sync-phase-profile.js +303 -0
  10. package/dist/benchmark/sync-phase-profile.js.map +1 -0
  11. package/dist/src/checked-prune.d.ts +55 -0
  12. package/dist/src/checked-prune.d.ts.map +1 -0
  13. package/dist/src/checked-prune.js +244 -0
  14. package/dist/src/checked-prune.js.map +1 -0
  15. package/dist/src/index.d.ts +10 -9
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/index.js +285 -186
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/sync/index.d.ts +9 -1
  20. package/dist/src/sync/index.d.ts.map +1 -1
  21. package/dist/src/sync/profile.d.ts +3 -0
  22. package/dist/src/sync/profile.d.ts.map +1 -0
  23. package/dist/src/sync/profile.js +2 -0
  24. package/dist/src/sync/profile.js.map +1 -0
  25. package/dist/src/sync/rateless-iblt.d.ts +25 -11
  26. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  27. package/dist/src/sync/rateless-iblt.js +597 -106
  28. package/dist/src/sync/rateless-iblt.js.map +1 -1
  29. package/dist/src/sync/simple.d.ts +5 -0
  30. package/dist/src/sync/simple.d.ts.map +1 -1
  31. package/dist/src/sync/simple.js +224 -74
  32. package/dist/src/sync/simple.js.map +1 -1
  33. package/package.json +16 -12
  34. package/src/checked-prune.ts +331 -0
  35. package/src/index.ts +451 -302
  36. package/src/sync/index.ts +11 -1
  37. package/src/sync/profile.ts +9 -0
  38. package/src/sync/rateless-iblt.ts +768 -128
  39. package/src/sync/simple.ts +256 -94
@@ -1,4 +1,9 @@
1
- import { field, variant, vec } from "@dao-xyz/borsh";
1
+ import {
2
+ type BinaryReader,
3
+ type BinaryWriter,
4
+ field,
5
+ variant,
6
+ } from "@dao-xyz/borsh";
2
7
  import { Cache } from "@peerbit/cache";
3
8
  import { type PublicSignKey, randomBytes, toBase64 } from "@peerbit/crypto";
4
9
  import {
@@ -28,7 +33,16 @@ import type {
28
33
  SynchronizerComponents,
29
34
  Syncronizer,
30
35
  } from "./index.js";
31
- import { SimpleSyncronizer } from "./simple.js";
36
+ import {
37
+ type SyncProfileFn,
38
+ emitSyncProfileDuration,
39
+ emitSyncProfileEvent,
40
+ syncProfileStart,
41
+ } from "./profile.js";
42
+ import {
43
+ SYNC_MESSAGE_PRIORITY,
44
+ SimpleSyncronizer,
45
+ } from "./simple.js";
32
46
 
33
47
  export const logger = loggerFn("peerbit:shared-log:rateless");
34
48
 
@@ -37,6 +51,12 @@ type NumberOrBigint = number | bigint;
37
51
  const coerceBigInt = (value: NumberOrBigint): bigint =>
38
52
  typeof value === "bigint" ? value : BigInt(value);
39
53
 
54
+ export interface SSymbol {
55
+ count: bigint;
56
+ hash: bigint;
57
+ symbol: bigint;
58
+ }
59
+
40
60
  class SymbolSerialized implements SSymbol {
41
61
  @field({ type: "u64" })
42
62
  count: bigint;
@@ -54,6 +74,388 @@ class SymbolSerialized implements SSymbol {
54
74
  }
55
75
  }
56
76
 
77
+ const CODED_SYMBOL_WORDS = 3;
78
+ const CODED_SYMBOL_WORD_BYTES = 8;
79
+ const CODED_SYMBOL_BYTES = CODED_SYMBOL_WORDS * CODED_SYMBOL_WORD_BYTES;
80
+ const BIG_UINT64_ARRAY_IS_LITTLE_ENDIAN =
81
+ typeof BigUint64Array !== "undefined" &&
82
+ new Uint8Array(new BigUint64Array([1n]).buffer)[0] === 1;
83
+
84
+ type CodedSymbol = SSymbol | SymbolSerialized;
85
+ type CodedSymbolInput =
86
+ | CodedSymbolBatch
87
+ | readonly CodedSymbol[]
88
+ | BigUint64Array;
89
+
90
+ const assertValidFlatCodedSymbols = (flat: BigUint64Array) => {
91
+ if (flat.length % CODED_SYMBOL_WORDS !== 0) {
92
+ throw new Error("Invalid RIBLT coded symbol batch");
93
+ }
94
+ };
95
+
96
+ export class CodedSymbolBatch implements Iterable<CodedSymbol> {
97
+ private flat?: BigUint64Array;
98
+ private readonly symbols?: readonly CodedSymbol[];
99
+
100
+ private constructor(properties: {
101
+ flat?: BigUint64Array;
102
+ symbols?: readonly CodedSymbol[];
103
+ }) {
104
+ this.flat = properties.flat;
105
+ this.symbols = properties.symbols;
106
+ }
107
+
108
+ static from(symbols: CodedSymbolInput): CodedSymbolBatch {
109
+ if (symbols instanceof CodedSymbolBatch) {
110
+ return symbols;
111
+ }
112
+
113
+ if (
114
+ typeof BigUint64Array !== "undefined" &&
115
+ symbols instanceof BigUint64Array
116
+ ) {
117
+ return CodedSymbolBatch.fromFlat(symbols);
118
+ }
119
+
120
+ return CodedSymbolBatch.fromSymbols(symbols as readonly CodedSymbol[]);
121
+ }
122
+
123
+ static fromFlat(flat: BigUint64Array): CodedSymbolBatch {
124
+ assertValidFlatCodedSymbols(flat);
125
+ return new CodedSymbolBatch({ flat });
126
+ }
127
+
128
+ static fromSymbols(symbols: readonly CodedSymbol[]): CodedSymbolBatch {
129
+ return new CodedSymbolBatch({ symbols });
130
+ }
131
+
132
+ get length(): number {
133
+ return this.flat
134
+ ? this.flat.length / CODED_SYMBOL_WORDS
135
+ : (this.symbols?.length ?? 0);
136
+ }
137
+
138
+ toFlat(): BigUint64Array {
139
+ if (this.flat) {
140
+ return this.flat;
141
+ }
142
+
143
+ const symbols = this.symbols ?? [];
144
+ const flat = new BigUint64Array(symbols.length * CODED_SYMBOL_WORDS);
145
+ for (let i = 0; i < symbols.length; i++) {
146
+ const offset = i * CODED_SYMBOL_WORDS;
147
+ const symbol = symbols[i];
148
+ flat[offset] = coerceBigInt(symbol.count);
149
+ flat[offset + 1] = coerceBigInt(symbol.hash);
150
+ flat[offset + 2] = coerceBigInt(symbol.symbol);
151
+ }
152
+ this.flat = flat;
153
+ return flat;
154
+ }
155
+
156
+ toSymbols(): SymbolSerialized[] {
157
+ const symbols: SymbolSerialized[] = [];
158
+ for (const symbol of this) {
159
+ symbols.push(
160
+ symbol instanceof SymbolSerialized
161
+ ? symbol
162
+ : new SymbolSerialized({
163
+ count: symbol.count,
164
+ hash: symbol.hash,
165
+ symbol: symbol.symbol,
166
+ }),
167
+ );
168
+ }
169
+ return symbols;
170
+ }
171
+
172
+ *[Symbol.iterator](): IterableIterator<CodedSymbol> {
173
+ if (this.symbols) {
174
+ yield* this.symbols;
175
+ return;
176
+ }
177
+
178
+ const flat = this.flat;
179
+ if (!flat) {
180
+ return;
181
+ }
182
+
183
+ for (let i = 0; i < flat.length; i += CODED_SYMBOL_WORDS) {
184
+ yield {
185
+ count: flat[i],
186
+ hash: flat[i + 1],
187
+ symbol: flat[i + 2],
188
+ };
189
+ }
190
+ }
191
+ }
192
+
193
+ const codedSymbolBatchField = {
194
+ serialize: (symbols: CodedSymbolInput, writer: BinaryWriter) => {
195
+ const batch = CodedSymbolBatch.from(symbols);
196
+ const length = batch.length;
197
+ writer.u32(length);
198
+ if (length === 0) {
199
+ return;
200
+ }
201
+
202
+ if (typeof BigUint64Array !== "undefined") {
203
+ const flat = batch.toFlat();
204
+ if (BIG_UINT64_ARRAY_IS_LITTLE_ENDIAN) {
205
+ writer.set(
206
+ new Uint8Array(flat.buffer, flat.byteOffset, flat.byteLength),
207
+ );
208
+ return;
209
+ }
210
+
211
+ for (let i = 0; i < flat.length; i++) {
212
+ writer.u64(flat[i]);
213
+ }
214
+ return;
215
+ }
216
+
217
+ for (const symbol of batch) {
218
+ writer.u64(symbol.count);
219
+ writer.u64(symbol.hash);
220
+ writer.u64(symbol.symbol);
221
+ }
222
+ },
223
+ deserialize: (reader: BinaryReader): CodedSymbolBatch => {
224
+ const length = reader.u32();
225
+ const wordLength = length * CODED_SYMBOL_WORDS;
226
+ const byteLength = length * CODED_SYMBOL_BYTES;
227
+
228
+ if (
229
+ typeof BigUint64Array !== "undefined" &&
230
+ BIG_UINT64_ARRAY_IS_LITTLE_ENDIAN
231
+ ) {
232
+ if (reader._offset + byteLength > reader._buf.length) {
233
+ throw new Error("Invalid RIBLT coded symbol batch length");
234
+ }
235
+ const bytes = reader.buffer(byteLength);
236
+ const flat = new BigUint64Array(wordLength);
237
+ new Uint8Array(flat.buffer).set(bytes);
238
+ return CodedSymbolBatch.fromFlat(flat);
239
+ }
240
+
241
+ const symbols: SymbolSerialized[] = [];
242
+ for (let i = 0; i < length; i++) {
243
+ symbols.push(
244
+ new SymbolSerialized({
245
+ count: reader.u64(),
246
+ hash: reader.u64(),
247
+ symbol: reader.u64(),
248
+ }),
249
+ );
250
+ }
251
+ return CodedSymbolBatch.fromSymbols(symbols);
252
+ },
253
+ };
254
+
255
+ type RibltSymbolAdder = {
256
+ add_symbol: (symbol: bigint) => void;
257
+ add_symbols?: (symbols: BigUint64Array) => void;
258
+ };
259
+
260
+ type BatchEncoderWrapper = EncoderWrapper & {
261
+ produce_next_coded_symbols?: (count: number) => BigUint64Array;
262
+ };
263
+
264
+ type StartSyncEncoderWrapper = EncoderWrapper & {
265
+ add_symbols_sorted_and_find_range?: (
266
+ symbols: BigUint64Array,
267
+ maxValue: bigint,
268
+ ) => BigUint64Array;
269
+ add_symbols_sorted_find_range_and_produce?: (
270
+ symbols: BigUint64Array,
271
+ maxValue: bigint,
272
+ count: number,
273
+ ) => BigUint64Array;
274
+ };
275
+
276
+ type BatchDecoderWrapper = DecoderWrapper & {
277
+ add_symbols?: (symbols: BigUint64Array) => void;
278
+ add_coded_symbols_and_try_decode?: (symbols: BigUint64Array) => boolean;
279
+ get_remote_symbol_values?: () => BigUint64Array;
280
+ };
281
+
282
+ const addSymbolsToRiblt = (
283
+ target: RibltSymbolAdder,
284
+ symbols: Iterable<NumberOrBigint> | BigUint64Array,
285
+ ) => {
286
+ if (
287
+ typeof BigUint64Array !== "undefined" &&
288
+ typeof target.add_symbols === "function"
289
+ ) {
290
+ target.add_symbols(
291
+ symbols instanceof BigUint64Array
292
+ ? symbols
293
+ : BigUint64Array.from(symbols, coerceBigInt),
294
+ );
295
+ return;
296
+ }
297
+
298
+ for (const symbol of symbols) {
299
+ target.add_symbol(coerceBigInt(symbol));
300
+ }
301
+ };
302
+
303
+ const produceNextCodedSymbols = (
304
+ encoder: EncoderWrapper,
305
+ count: number,
306
+ ): CodedSymbolBatch => {
307
+ const produceBatch = (encoder as BatchEncoderWrapper)
308
+ .produce_next_coded_symbols;
309
+ if (typeof BigUint64Array !== "undefined" && produceBatch) {
310
+ return CodedSymbolBatch.fromFlat(produceBatch.call(encoder, count));
311
+ }
312
+
313
+ const symbols: SymbolSerialized[] = [];
314
+ for (let i = 0; i < count; i++) {
315
+ symbols.push(new SymbolSerialized(encoder.produce_next_coded_symbol()));
316
+ }
317
+ return CodedSymbolBatch.fromSymbols(symbols);
318
+ };
319
+
320
+ const flatFromCodedSymbols = (symbols: CodedSymbolInput): BigUint64Array => {
321
+ if (
322
+ typeof BigUint64Array !== "undefined" &&
323
+ symbols instanceof BigUint64Array
324
+ ) {
325
+ assertValidFlatCodedSymbols(symbols);
326
+ return symbols;
327
+ }
328
+ return CodedSymbolBatch.from(symbols).toFlat();
329
+ };
330
+
331
+ const getRemoteSymbolValues = (decoder: DecoderWrapper): bigint[] => {
332
+ const getBatch = (decoder as BatchDecoderWrapper).get_remote_symbol_values;
333
+ if (typeof BigUint64Array !== "undefined" && getBatch) {
334
+ return Array.from(getBatch.call(decoder));
335
+ }
336
+
337
+ const symbols: bigint[] = [];
338
+ for (const missingSymbol of decoder.get_remote_symbols()) {
339
+ symbols.push(coerceBigInt(missingSymbol));
340
+ }
341
+ return symbols;
342
+ };
343
+
344
+ const prepareStartSyncEncoder = (
345
+ coordinates: readonly bigint[],
346
+ maxValue: NumberOrBigint,
347
+ initialSymbolCount: number,
348
+ ): {
349
+ encoder: EncoderWrapper;
350
+ start: bigint;
351
+ end: bigint;
352
+ initialSymbols?: CodedSymbolBatch;
353
+ } => {
354
+ const encoder = new EncoderWrapper();
355
+ let complete = false;
356
+ try {
357
+ const prepareAndProduceNative = (encoder as StartSyncEncoderWrapper)
358
+ .add_symbols_sorted_find_range_and_produce;
359
+ if (typeof BigUint64Array !== "undefined" && prepareAndProduceNative) {
360
+ const prepared = prepareAndProduceNative.call(
361
+ encoder,
362
+ BigUint64Array.from(coordinates),
363
+ coerceBigInt(maxValue),
364
+ initialSymbolCount,
365
+ );
366
+ if (
367
+ prepared.length < 2 ||
368
+ (prepared.length - 2) % CODED_SYMBOL_WORDS !== 0
369
+ ) {
370
+ throw new Error("Invalid RIBLT prepared encoder result");
371
+ }
372
+ complete = true;
373
+ return {
374
+ encoder,
375
+ start: prepared[0],
376
+ end: prepared[1],
377
+ initialSymbols: CodedSymbolBatch.fromFlat(prepared.subarray(2)),
378
+ };
379
+ }
380
+
381
+ const prepareNative = (encoder as StartSyncEncoderWrapper)
382
+ .add_symbols_sorted_and_find_range;
383
+ if (typeof BigUint64Array !== "undefined" && prepareNative) {
384
+ const range = prepareNative.call(
385
+ encoder,
386
+ BigUint64Array.from(coordinates),
387
+ coerceBigInt(maxValue),
388
+ );
389
+ if (range.length !== 2) {
390
+ throw new Error("Invalid RIBLT range result");
391
+ }
392
+ complete = true;
393
+ return { encoder, start: range[0], end: range[1] };
394
+ }
395
+
396
+ let sortedEntries: bigint[] | BigUint64Array;
397
+ if (typeof BigUint64Array !== "undefined") {
398
+ const typed = new BigUint64Array(coordinates.length);
399
+ for (let i = 0; i < coordinates.length; i++) {
400
+ typed[i] = coordinates[i];
401
+ }
402
+ typed.sort();
403
+ sortedEntries = typed;
404
+ } else {
405
+ sortedEntries = [...coordinates].sort((a, b) => {
406
+ if (a > b) {
407
+ return 1;
408
+ } else if (a < b) {
409
+ return -1;
410
+ } else {
411
+ return 0;
412
+ }
413
+ });
414
+ }
415
+
416
+ // assume sorted, and find the largest gap
417
+ let largestGap = 0n;
418
+ let largestGapIndex = 0;
419
+ for (let i = 0; i < sortedEntries.length; i++) {
420
+ const current = sortedEntries[i];
421
+ const next = sortedEntries[(i + 1) % sortedEntries.length];
422
+ const gap =
423
+ next >= current
424
+ ? next - current
425
+ : coerceBigInt(maxValue) - current + next;
426
+ if (gap > largestGap) {
427
+ largestGap = gap;
428
+ largestGapIndex = i;
429
+ }
430
+ }
431
+
432
+ const smallestRangeStartIndex =
433
+ (largestGapIndex + 1) % sortedEntries.length;
434
+ const smallestRangeEndIndex = largestGapIndex; /// === (smallRangeStartIndex + 1) % sortedEntries.length
435
+ let smallestRangeStart = sortedEntries[smallestRangeStartIndex];
436
+ let smallestRangeEnd = sortedEntries[smallestRangeEndIndex];
437
+ let start: bigint, end: bigint;
438
+ if (smallestRangeEnd === smallestRangeStart) {
439
+ start = smallestRangeEnd;
440
+ end = smallestRangeEnd + 1n;
441
+ if (end > maxValue) {
442
+ end = 0n;
443
+ }
444
+ } else {
445
+ start = smallestRangeStart;
446
+ end = smallestRangeEnd;
447
+ }
448
+
449
+ addSymbolsToRiblt(encoder, sortedEntries);
450
+ complete = true;
451
+ return { encoder, start, end };
452
+ } finally {
453
+ if (!complete) {
454
+ encoder.free();
455
+ }
456
+ }
457
+ };
458
+
57
459
  const getSyncIdString = (message: { syncId: Uint8Array }) => {
58
460
  return toBase64(message.syncId);
59
461
  };
@@ -61,6 +463,8 @@ const getSyncIdString = (message: { syncId: Uint8Array }) => {
61
463
  const DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS = 30_000;
62
464
  const DEFAULT_CONVERGENT_RETRY_INTERVALS_MS = [0, 1_000, 3_000, 7_000];
63
465
  const DEFAULT_MAX_CONVERGENT_TRACKED_HASHES = 4_096;
466
+ const MIN_MORE_SYMBOLS_BATCH_SIZE = 64;
467
+ const MAX_MORE_SYMBOLS_BATCH_SIZE = 1_024;
64
468
 
65
469
  @variant([3, 0])
66
470
  export class StartSync extends TransportMessage {
@@ -73,19 +477,19 @@ export class StartSync extends TransportMessage {
73
477
  @field({ type: "u64" })
74
478
  end: bigint;
75
479
 
76
- @field({ type: vec(SymbolSerialized) })
77
- symbols: SymbolSerialized[];
480
+ @field({ type: codedSymbolBatchField })
481
+ symbols: CodedSymbolBatch;
78
482
 
79
483
  constructor(props: {
80
484
  from: NumberOrBigint;
81
485
  to: NumberOrBigint;
82
- symbols: SymbolSerialized[];
486
+ symbols: CodedSymbolInput;
83
487
  }) {
84
488
  super();
85
489
  this.syncId = randomBytes(32);
86
490
  this.start = coerceBigInt(props.from);
87
491
  this.end = coerceBigInt(props.to);
88
- this.symbols = props.symbols;
492
+ this.symbols = CodedSymbolBatch.from(props.symbols);
89
493
  }
90
494
  }
91
495
 
@@ -97,18 +501,18 @@ export class MoreSymbols extends TransportMessage {
97
501
  @field({ type: "u64" })
98
502
  seqNo: bigint;
99
503
 
100
- @field({ type: vec(SymbolSerialized) })
101
- symbols: SymbolSerialized[];
504
+ @field({ type: codedSymbolBatchField })
505
+ symbols: CodedSymbolBatch;
102
506
 
103
507
  constructor(props: {
104
508
  syncId: Uint8Array;
105
509
  lastSeqNo: bigint;
106
- symbols: SymbolSerialized[];
510
+ symbols: CodedSymbolInput;
107
511
  }) {
108
512
  super();
109
513
  this.syncId = props.syncId;
110
514
  this.seqNo = props.lastSeqNo + 1n;
111
- this.symbols = props.symbols;
515
+ this.symbols = CodedSymbolBatch.from(props.symbols);
112
516
  }
113
517
  }
114
518
 
@@ -138,12 +542,6 @@ export class RequestAll extends TransportMessage {
138
542
  }
139
543
  }
140
544
 
141
- export interface SSymbol {
142
- count: bigint;
143
- hash: bigint;
144
- symbol: bigint;
145
- }
146
-
147
545
  const matchEntriesByHashNumberInRangeQuery = (range: {
148
546
  start1: number | bigint;
149
547
  end1: number | bigint;
@@ -201,11 +599,13 @@ const buildEncoderOrDecoderFromRange = async <
201
599
  },
202
600
  entryIndex: Index<EntryReplicated<D>>,
203
601
  type: T,
602
+ profile?: SyncProfileFn,
204
603
  ): Promise<E | false> => {
205
604
  await ribltReady;
206
605
  const encoder =
207
606
  type === "encoder" ? new EncoderWrapper() : new DecoderWrapper();
208
607
 
608
+ const rangeQueryStartedAt = syncProfileStart(profile);
209
609
  const entries = await entryIndex
210
610
  .iterate(
211
611
  {
@@ -225,13 +625,40 @@ const buildEncoderOrDecoderFromRange = async <
225
625
  },
226
626
  )
227
627
  .all();
628
+ if (profile) {
629
+ emitSyncProfileDuration(profile, rangeQueryStartedAt, {
630
+ name: "rateless.rangeQuery",
631
+ entries: entries.length,
632
+ details: { type },
633
+ });
634
+ }
228
635
 
229
636
  if (entries.length === 0) {
230
637
  return false;
231
638
  }
232
639
 
233
- for (const entry of entries) {
234
- encoder.add_symbol(coerceBigInt(entry.value.hashNumber));
640
+ const addSymbolsStartedAt = syncProfileStart(profile);
641
+ if (
642
+ typeof BigUint64Array !== "undefined" &&
643
+ typeof (encoder as RibltSymbolAdder).add_symbols === "function"
644
+ ) {
645
+ const symbols = new BigUint64Array(entries.length);
646
+ for (let i = 0; i < entries.length; i++) {
647
+ symbols[i] = coerceBigInt(entries[i].value.hashNumber);
648
+ }
649
+ addSymbolsToRiblt(encoder as RibltSymbolAdder, symbols);
650
+ } else {
651
+ for (const entry of entries) {
652
+ encoder.add_symbol(coerceBigInt(entry.value.hashNumber));
653
+ }
654
+ }
655
+ if (profile) {
656
+ emitSyncProfileDuration(profile, addSymbolsStartedAt, {
657
+ name: "rateless.rangeAddSymbols",
658
+ entries: entries.length,
659
+ symbols: entries.length,
660
+ details: { type },
661
+ });
235
662
  }
236
663
  return encoder as E;
237
664
  };
@@ -258,7 +685,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
258
685
  refresh: () => void;
259
686
  process: (message: {
260
687
  seqNo: bigint;
261
- symbols: SSymbol[];
688
+ symbols: CodedSymbolInput;
262
689
  }) => Promise<boolean | undefined>;
263
690
  free: () => void;
264
691
  }
@@ -271,7 +698,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
271
698
  encoder: EncoderWrapper;
272
699
  timeout: ReturnType<typeof setTimeout>;
273
700
  refresh: () => void;
274
- next: (message: { lastSeqNo: bigint }) => SSymbol[];
701
+ next: (message: { lastSeqNo: bigint }) => CodedSymbolBatch;
275
702
  free: () => void;
276
703
  }
277
704
  >;
@@ -309,8 +736,11 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
309
736
  }
310
737
 
311
738
  let index = 0;
312
- const scored: { entry: EntryReplicated<D>; index: number; priority: number }[] =
313
- [];
739
+ const scored: {
740
+ entry: EntryReplicated<D>;
741
+ index: number;
742
+ priority: number;
743
+ }[] = [];
314
744
  for (const entry of entries.values()) {
315
745
  const priorityValue = priorityFn(entry);
316
746
  scored.push({
@@ -337,7 +767,9 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
337
767
  1,
338
768
  Math.floor(properties.timeoutMs ?? DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS),
339
769
  );
340
- const retryIntervalsMs = this.normalizeRetryIntervals(properties.retryIntervalsMs);
770
+ const retryIntervalsMs = this.normalizeRetryIntervals(
771
+ properties.retryIntervalsMs,
772
+ );
341
773
  const trackedLimit = this.maxConvergentTrackedHashes;
342
774
  const requestedHashes = [...properties.entries.keys()];
343
775
  const requestedHashesTracked = requestedHashes.slice(0, trackedLimit);
@@ -487,19 +919,39 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
487
919
  start2: NumberOrBigint;
488
920
  end2: NumberOrBigint;
489
921
  }): Promise<DecoderWrapper | false> {
922
+ const profile = this.properties.sync?.profile;
490
923
  const key = this.localRangeEncoderCacheKey(ranges);
491
924
  const cached = this.localRangeEncoderCache.get(key);
492
925
  if (cached && cached.version === this.localRangeEncoderCacheVersion) {
926
+ const startedAt = syncProfileStart(profile);
493
927
  cached.lastUsed = Date.now();
494
- return this.decoderFromCachedEncoder(cached.encoder);
928
+ try {
929
+ return this.decoderFromCachedEncoder(cached.encoder);
930
+ } finally {
931
+ if (profile) {
932
+ emitSyncProfileDuration(profile, startedAt, {
933
+ name: "rateless.localDecoder",
934
+ cacheHit: true,
935
+ });
936
+ }
937
+ }
495
938
  }
496
939
 
940
+ const startedAt = syncProfileStart(profile);
497
941
  const encoder = (await buildEncoderOrDecoderFromRange(
498
942
  ranges,
499
943
  this.properties.entryIndex,
500
944
  "encoder",
945
+ profile,
501
946
  )) as EncoderWrapper | false;
502
947
  if (!encoder) {
948
+ if (profile) {
949
+ emitSyncProfileDuration(profile, startedAt, {
950
+ name: "rateless.localDecoder",
951
+ cacheHit: false,
952
+ entries: 0,
953
+ });
954
+ }
503
955
  return false;
504
956
  }
505
957
 
@@ -533,13 +985,24 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
533
985
  this.localRangeEncoderCache.delete(oldestKey);
534
986
  }
535
987
 
536
- return this.decoderFromCachedEncoder(encoder);
988
+ try {
989
+ return this.decoderFromCachedEncoder(encoder);
990
+ } finally {
991
+ if (profile) {
992
+ emitSyncProfileDuration(profile, startedAt, {
993
+ name: "rateless.localDecoder",
994
+ cacheHit: false,
995
+ });
996
+ }
997
+ }
537
998
  }
538
999
 
539
1000
  async onMaybeMissingEntries(properties: {
540
1001
  entries: Map<string, EntryReplicated<D>>;
541
1002
  targets: string[];
542
1003
  }): Promise<void> {
1004
+ const profile = this.properties.sync?.profile;
1005
+ const startedAt = syncProfileStart(profile);
543
1006
  // NOTE: this method is best-effort dispatch, not a per-hash convergence API.
544
1007
  // It may require follow-up repair rounds under churn/loss to fully close all gaps.
545
1008
  // Strategy:
@@ -554,13 +1017,33 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
554
1017
 
555
1018
  // Small batch => use simple synchronizer entirely
556
1019
  if (properties.entries.size <= minSyncIbltSize) {
557
- await this.simple.onMaybeMissingEntries({
558
- entries: properties.entries,
559
- targets: properties.targets,
560
- });
1020
+ if (profile) {
1021
+ emitSyncProfileEvent(profile, {
1022
+ name: "rateless.dispatchMode",
1023
+ entries: properties.entries.size,
1024
+ targets: properties.targets.length,
1025
+ details: { mode: "simple-small" },
1026
+ });
1027
+ }
1028
+ try {
1029
+ await this.simple.onMaybeMissingEntries({
1030
+ entries: properties.entries,
1031
+ targets: properties.targets,
1032
+ });
1033
+ } finally {
1034
+ if (profile) {
1035
+ emitSyncProfileDuration(profile, startedAt, {
1036
+ name: "rateless.onMaybeMissingEntries",
1037
+ entries: properties.entries.size,
1038
+ targets: properties.targets.length,
1039
+ details: { mode: "simple-small" },
1040
+ });
1041
+ }
1042
+ }
561
1043
  return;
562
1044
  }
563
1045
 
1046
+ const selectStartedAt = syncProfileStart(profile);
564
1047
  const nonBoundaryEntries: EntryReplicated<D>[] = [];
565
1048
  for (const entry of properties.entries.values()) {
566
1049
  if (entry.assignedToRangeBoundary) {
@@ -635,104 +1118,103 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
635
1118
  }
636
1119
  }
637
1120
 
638
- if (allCoordinatesToSyncWithIblt.length === 0) {
639
- return;
640
- }
641
-
642
- await ribltReady;
643
-
644
- let sortedEntries: bigint[] | BigUint64Array;
645
- if (typeof BigUint64Array !== "undefined") {
646
- const typed = new BigUint64Array(allCoordinatesToSyncWithIblt.length);
647
- for (let i = 0; i < allCoordinatesToSyncWithIblt.length; i++) {
648
- typed[i] = allCoordinatesToSyncWithIblt[i];
649
- }
650
- typed.sort();
651
- sortedEntries = typed;
652
- } else {
653
- sortedEntries = allCoordinatesToSyncWithIblt.sort((a, b) => {
654
- if (a > b) {
655
- return 1;
656
- } else if (a < b) {
657
- return -1;
658
- } else {
659
- return 0;
660
- }
1121
+ if (profile) {
1122
+ emitSyncProfileDuration(profile, selectStartedAt, {
1123
+ name: "rateless.selectEntries",
1124
+ entries: properties.entries.size,
1125
+ symbols: allCoordinatesToSyncWithIblt.length,
1126
+ targets: properties.targets.length,
1127
+ details: {
1128
+ naiveEntries: entriesToSyncNaively.size,
1129
+ priority: priorityFn != null,
1130
+ },
661
1131
  });
662
1132
  }
663
1133
 
664
- // assume sorted, and find the largest gap
665
- let largestGap = 0n;
666
- let largestGapIndex = 0;
667
- for (let i = 0; i < sortedEntries.length; i++) {
668
- const current = sortedEntries[i];
669
- const next = sortedEntries[(i + 1) % sortedEntries.length];
670
- const gap =
671
- next >= current
672
- ? next - current
673
- : coerceBigInt(this.properties.numbers.maxValue) - current + next;
674
- if (gap > largestGap) {
675
- largestGap = gap;
676
- largestGapIndex = i;
677
- }
678
- }
679
-
680
- const smallestRangeStartIndex =
681
- (largestGapIndex + 1) % sortedEntries.length;
682
- const smallestRangeEndIndex = largestGapIndex; /// === (smallRangeStartIndex + 1) % sortedEntries.length
683
- let smallestRangeStart = sortedEntries[smallestRangeStartIndex];
684
- let smallestRangeEnd = sortedEntries[smallestRangeEndIndex];
685
- let start: bigint, end: bigint;
686
- if (smallestRangeEnd === smallestRangeStart) {
687
- start = smallestRangeEnd;
688
- end = smallestRangeEnd + 1n;
689
- if (end > this.properties.numbers.maxValue) {
690
- end = 0n;
1134
+ if (allCoordinatesToSyncWithIblt.length === 0) {
1135
+ if (profile) {
1136
+ emitSyncProfileEvent(profile, {
1137
+ name: "rateless.dispatchMode",
1138
+ entries: properties.entries.size,
1139
+ targets: properties.targets.length,
1140
+ details: { mode: "simple-only" },
1141
+ });
1142
+ emitSyncProfileDuration(profile, startedAt, {
1143
+ name: "rateless.onMaybeMissingEntries",
1144
+ entries: properties.entries.size,
1145
+ targets: properties.targets.length,
1146
+ details: { mode: "simple-only" },
1147
+ });
691
1148
  }
692
- } else {
693
- start = smallestRangeStart;
694
- end = smallestRangeEnd;
1149
+ return;
695
1150
  }
696
1151
 
697
- const startSync = new StartSync({ from: start, to: end, symbols: [] });
698
- const encoder = new EncoderWrapper();
699
- if (
700
- typeof BigUint64Array !== "undefined" &&
701
- sortedEntries instanceof BigUint64Array
702
- ) {
703
- encoder.add_symbols(sortedEntries);
704
- } else {
705
- for (const entry of sortedEntries) {
706
- encoder.add_symbol(coerceBigInt(entry));
707
- }
708
- }
1152
+ await ribltReady;
709
1153
 
710
1154
  // For smaller sets, the original `sqrt(n)` heuristic can occasionally under-provision
711
1155
  // low-degree symbols early, causing an unnecessary `MoreSymbols` round-trip. Use a
712
1156
  // small floor to make small-delta syncs more reliable without affecting large-n behavior.
713
- let initialSymbols = Math.round(
1157
+ let initialSymbolCount = Math.round(
714
1158
  Math.sqrt(allCoordinatesToSyncWithIblt.length),
715
1159
  ); // TODO choose better
716
- initialSymbols = Math.max(64, initialSymbols);
717
- for (let i = 0; i < initialSymbols; i++) {
718
- startSync.symbols.push(
719
- new SymbolSerialized(encoder.produce_next_coded_symbol()),
720
- );
1160
+ initialSymbolCount = Math.max(64, initialSymbolCount);
1161
+ const prepareStartedAt = syncProfileStart(profile);
1162
+ const { encoder, start, end, initialSymbols } = prepareStartSyncEncoder(
1163
+ allCoordinatesToSyncWithIblt,
1164
+ this.properties.numbers.maxValue,
1165
+ initialSymbolCount,
1166
+ );
1167
+ if (profile) {
1168
+ emitSyncProfileDuration(profile, prepareStartedAt, {
1169
+ name: "rateless.prepareStartSyncEncoder",
1170
+ entries: allCoordinatesToSyncWithIblt.length,
1171
+ symbols: initialSymbols?.length,
1172
+ details: {
1173
+ initialSymbolCount,
1174
+ includesInitialSymbols: initialSymbols != null,
1175
+ },
1176
+ });
1177
+ }
1178
+
1179
+ let startSyncSymbols = initialSymbols;
1180
+ if (!startSyncSymbols) {
1181
+ const produceStartedAt = syncProfileStart(profile);
1182
+ startSyncSymbols = produceNextCodedSymbols(encoder, initialSymbolCount);
1183
+ if (profile) {
1184
+ emitSyncProfileDuration(profile, produceStartedAt, {
1185
+ name: "rateless.produceStartSyncSymbols",
1186
+ symbols: startSyncSymbols.length,
1187
+ });
1188
+ }
721
1189
  }
722
1190
 
1191
+ const startSync = new StartSync({
1192
+ from: start,
1193
+ to: end,
1194
+ symbols: startSyncSymbols,
1195
+ });
1196
+ const syncId = getSyncIdString(startSync);
1197
+
723
1198
  const clear = () => {
724
1199
  encoder.free();
725
- clearTimeout(
726
- this.outgoingSyncProcesses.get(getSyncIdString(startSync))?.timeout,
727
- );
728
- this.outgoingSyncProcesses.delete(getSyncIdString(startSync));
1200
+ clearTimeout(this.outgoingSyncProcesses.get(syncId)?.timeout);
1201
+ this.outgoingSyncProcesses.delete(syncId);
729
1202
  };
730
1203
  const createTimeout = () => {
731
1204
  return setTimeout(clear, 1e4); // TODO arg
732
1205
  };
733
1206
 
734
1207
  let lastSeqNo = -1n;
735
- let nextBatch = 1e4;
1208
+ // Keep follow-up symbol payloads bounded. Each symbol is serialized as an
1209
+ // object with three bigint fields, so very large batches can dominate heap under
1210
+ // concurrent churn even though the native RIBLT encoder itself is compact.
1211
+ const nextBatch = Math.max(
1212
+ MIN_MORE_SYMBOLS_BATCH_SIZE,
1213
+ Math.min(
1214
+ MAX_MORE_SYMBOLS_BATCH_SIZE,
1215
+ Math.ceil(allCoordinatesToSyncWithIblt.length / 4),
1216
+ ),
1217
+ );
736
1218
  const obj = {
737
1219
  encoder,
738
1220
  timeout: createTimeout(),
@@ -743,34 +1225,86 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
743
1225
  }
744
1226
  obj.timeout = createTimeout();
745
1227
  },
746
- next: (properties: { lastSeqNo: bigint }): SSymbol[] => {
1228
+ next: (properties: { lastSeqNo: bigint }): CodedSymbolBatch => {
747
1229
  if (properties.lastSeqNo <= lastSeqNo) {
748
- return [];
1230
+ return CodedSymbolBatch.fromSymbols([]);
749
1231
  }
750
1232
  lastSeqNo++;
751
1233
  obj.refresh(); // TODO use timestamp instead and collective pruning/refresh
752
1234
 
753
- let result: SSymbol[] = [];
754
- for (let i = 0; i < nextBatch; i++) {
755
- result.push(encoder.produce_next_coded_symbol());
1235
+ const produceStartedAt = syncProfileStart(profile);
1236
+ const symbols = produceNextCodedSymbols(encoder, nextBatch);
1237
+ if (profile) {
1238
+ emitSyncProfileDuration(profile, produceStartedAt, {
1239
+ name: "rateless.produceMoreSymbols",
1240
+ syncId,
1241
+ symbols: symbols.length,
1242
+ });
756
1243
  }
757
- return result;
1244
+ return symbols;
758
1245
  },
759
1246
  free: clear,
760
1247
  outgoing: properties.entries,
761
1248
  };
762
1249
 
763
- this.outgoingSyncProcesses.set(getSyncIdString(startSync), obj);
764
- this.simple.rpc.send(startSync, {
1250
+ this.outgoingSyncProcesses.set(syncId, obj);
1251
+ if (profile) {
1252
+ emitSyncProfileEvent(profile, {
1253
+ name: "rateless.dispatchMode",
1254
+ entries: properties.entries.size,
1255
+ symbols: startSyncSymbols.length,
1256
+ targets: properties.targets.length,
1257
+ syncId,
1258
+ details: { mode: "rateless" },
1259
+ });
1260
+ }
1261
+ const sendStartedAt = syncProfileStart(profile);
1262
+ const sendResult = this.simple.rpc.send(startSync, {
765
1263
  mode: new SilentDelivery({ to: properties.targets, redundancy: 1 }),
766
- priority: 1,
1264
+ priority: SYNC_MESSAGE_PRIORITY,
767
1265
  });
1266
+ if (profile) {
1267
+ void Promise.resolve(sendResult).then(
1268
+ () =>
1269
+ emitSyncProfileDuration(profile, sendStartedAt, {
1270
+ name: "rateless.sendStartSync",
1271
+ messages: 1,
1272
+ symbols: startSyncSymbols.length,
1273
+ targets: properties.targets.length,
1274
+ syncId,
1275
+ }),
1276
+ () =>
1277
+ emitSyncProfileDuration(profile, sendStartedAt, {
1278
+ name: "rateless.sendStartSync",
1279
+ messages: 1,
1280
+ symbols: startSyncSymbols.length,
1281
+ targets: properties.targets.length,
1282
+ syncId,
1283
+ details: { rejected: true },
1284
+ }),
1285
+ );
1286
+ }
1287
+ if (profile) {
1288
+ emitSyncProfileDuration(profile, startedAt, {
1289
+ name: "rateless.onMaybeMissingEntries",
1290
+ entries: properties.entries.size,
1291
+ messages: 1,
1292
+ symbols: startSyncSymbols.length,
1293
+ targets: properties.targets.length,
1294
+ details: {
1295
+ mode: "rateless",
1296
+ ibltEntries: allCoordinatesToSyncWithIblt.length,
1297
+ naiveEntries: entriesToSyncNaively.size,
1298
+ },
1299
+ });
1300
+ }
768
1301
  }
769
1302
 
770
1303
  async onMessage(
771
1304
  message: TransportMessage,
772
1305
  context: RequestContext,
773
1306
  ): Promise<boolean> {
1307
+ const profile = this.properties.sync?.profile;
774
1308
  if (message instanceof StartSync) {
775
1309
  const syncId = getSyncIdString(message);
776
1310
  if (this.ingoingSyncProcesses.has(syncId)) {
@@ -784,23 +1318,40 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
784
1318
  this.startedOrCompletedSynchronizations.add(syncId);
785
1319
 
786
1320
  const wrapped = message.end < message.start;
1321
+ const decoderStartedAt = syncProfileStart(profile);
787
1322
  const decoder = await this.getLocalDecoderForRange({
788
1323
  start1: message.start,
789
1324
  end1: wrapped ? this.properties.numbers.maxValue : message.end,
790
1325
  start2: 0n,
791
1326
  end2: wrapped ? message.end : 0n,
792
1327
  });
1328
+ if (profile) {
1329
+ emitSyncProfileDuration(profile, decoderStartedAt, {
1330
+ name: "rateless.getLocalDecoderForRange",
1331
+ syncId,
1332
+ details: { wrapped, found: decoder !== false },
1333
+ });
1334
+ }
793
1335
 
794
1336
  if (!decoder) {
1337
+ const sendStartedAt = syncProfileStart(profile);
795
1338
  await this.simple.rpc.send(
796
1339
  new RequestAll({
797
1340
  syncId: message.syncId,
798
1341
  }),
799
1342
  {
800
1343
  mode: new SilentDelivery({ to: [context.from!], redundancy: 1 }),
801
- priority: 1,
1344
+ priority: SYNC_MESSAGE_PRIORITY,
802
1345
  },
803
1346
  );
1347
+ if (profile) {
1348
+ emitSyncProfileDuration(profile, sendStartedAt, {
1349
+ name: "rateless.sendRequestAll",
1350
+ messages: 1,
1351
+ targets: 1,
1352
+ syncId,
1353
+ });
1354
+ }
804
1355
  return true;
805
1356
  }
806
1357
 
@@ -813,7 +1364,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
813
1364
 
814
1365
  let messageQueue: {
815
1366
  seqNo: bigint;
816
- symbols: (SSymbol | SymbolSerialized)[];
1367
+ symbols: CodedSymbolInput;
817
1368
  }[] = [];
818
1369
  let lastSeqNo = -1n;
819
1370
  const obj = {
@@ -828,7 +1379,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
828
1379
  },
829
1380
  process: async (newMessage: {
830
1381
  seqNo: bigint;
831
- symbols: (SSymbol | SymbolSerialized)[];
1382
+ symbols: CodedSymbolInput;
832
1383
  }): Promise<boolean | undefined> => {
833
1384
  obj.refresh(); // TODO use timestamp instead and collective pruning/refresh
834
1385
 
@@ -847,9 +1398,15 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
847
1398
  return false;
848
1399
  }
849
1400
 
850
- const allMissingSymbolsInRemote: bigint[] = [];
851
- for (const missingSymbol of decoder.get_remote_symbols()) {
852
- allMissingSymbolsInRemote.push(missingSymbol);
1401
+ const remoteStartedAt = syncProfileStart(profile);
1402
+ const allMissingSymbolsInRemote = getRemoteSymbolValues(decoder);
1403
+ if (profile) {
1404
+ emitSyncProfileDuration(profile, remoteStartedAt, {
1405
+ name: "rateless.remoteSymbols",
1406
+ entries: allMissingSymbolsInRemote.length,
1407
+ symbols: allMissingSymbolsInRemote.length,
1408
+ syncId,
1409
+ });
853
1410
  }
854
1411
 
855
1412
  // The IBLT decoder is based on a local snapshot. Entries can arrive via
@@ -871,7 +1428,41 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
871
1428
 
872
1429
  lastSeqNo = symbolMessage.seqNo;
873
1430
 
874
- for (const symbol of symbolMessage.symbols) {
1431
+ const addBatchAndDecode:
1432
+ | ((symbols: BigUint64Array) => boolean)
1433
+ | undefined = (decoder as BatchDecoderWrapper)
1434
+ .add_coded_symbols_and_try_decode;
1435
+ if (typeof BigUint64Array !== "undefined" && addBatchAndDecode) {
1436
+ const flatStartedAt = syncProfileStart(profile);
1437
+ const flatSymbols = flatFromCodedSymbols(symbolMessage.symbols);
1438
+ if (profile) {
1439
+ emitSyncProfileDuration(profile, flatStartedAt, {
1440
+ name: "rateless.symbolBatchToFlat",
1441
+ symbols: flatSymbols.length / CODED_SYMBOL_WORDS,
1442
+ syncId,
1443
+ });
1444
+ }
1445
+
1446
+ const decodeStartedAt = syncProfileStart(profile);
1447
+ const decoded = addBatchAndDecode.call(decoder, flatSymbols);
1448
+ if (profile) {
1449
+ emitSyncProfileDuration(profile, decodeStartedAt, {
1450
+ name: "rateless.decodeBatch",
1451
+ symbols: flatSymbols.length / CODED_SYMBOL_WORDS,
1452
+ syncId,
1453
+ details: { decoded },
1454
+ });
1455
+ }
1456
+ if (decoded && finalizeIfDecoded()) {
1457
+ return true;
1458
+ }
1459
+ continue;
1460
+ }
1461
+
1462
+ const decodeLoopStartedAt = syncProfileStart(profile);
1463
+ let symbolsProcessed = 0;
1464
+ for (const symbol of CodedSymbolBatch.from(symbolMessage.symbols)) {
1465
+ symbolsProcessed += 1;
875
1466
  const normalizedSymbol =
876
1467
  symbol instanceof SymbolSerialized
877
1468
  ? symbol
@@ -900,6 +1491,13 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
900
1491
  throw error;
901
1492
  }
902
1493
  }
1494
+ if (profile) {
1495
+ emitSyncProfileDuration(profile, decodeLoopStartedAt, {
1496
+ name: "rateless.decodeSymbolLoop",
1497
+ symbols: symbolsProcessed,
1498
+ syncId,
1499
+ });
1500
+ }
903
1501
  }
904
1502
  return false;
905
1503
  },
@@ -917,6 +1515,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
917
1515
  }
918
1516
 
919
1517
  // not done, request more symbols
1518
+ const sendStartedAt = syncProfileStart(profile);
920
1519
  await this.simple.rpc.send(
921
1520
  new RequestMoreSymbols({
922
1521
  lastSeqNo: 0n,
@@ -924,13 +1523,22 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
924
1523
  }),
925
1524
  {
926
1525
  mode: new SilentDelivery({ to: [context.from!], redundancy: 1 }),
927
- priority: 1,
1526
+ priority: SYNC_MESSAGE_PRIORITY,
928
1527
  },
929
1528
  );
1529
+ if (profile) {
1530
+ emitSyncProfileDuration(profile, sendStartedAt, {
1531
+ name: "rateless.sendRequestMoreSymbols",
1532
+ messages: 1,
1533
+ targets: 1,
1534
+ syncId,
1535
+ });
1536
+ }
930
1537
 
931
1538
  return true;
932
1539
  } else if (message instanceof MoreSymbols) {
933
- const obj = this.ingoingSyncProcesses.get(getSyncIdString(message));
1540
+ const syncId = getSyncIdString(message);
1541
+ const obj = this.ingoingSyncProcesses.get(syncId);
934
1542
  if (!obj) {
935
1543
  return true;
936
1544
  }
@@ -944,34 +1552,66 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
944
1552
 
945
1553
  // we are not done
946
1554
 
947
- this.simple.rpc.send(
1555
+ const sendStartedAt = syncProfileStart(profile);
1556
+ const sendResult = this.simple.rpc.send(
948
1557
  new RequestMoreSymbols({
949
1558
  lastSeqNo: message.seqNo,
950
1559
  syncId: message.syncId,
951
1560
  }),
952
1561
  {
953
1562
  mode: new SilentDelivery({ to: [context.from!], redundancy: 1 }),
954
- priority: 1,
1563
+ priority: SYNC_MESSAGE_PRIORITY,
955
1564
  },
956
1565
  );
1566
+ if (profile) {
1567
+ void Promise.resolve(sendResult).then(
1568
+ () =>
1569
+ emitSyncProfileDuration(profile, sendStartedAt, {
1570
+ name: "rateless.sendRequestMoreSymbols",
1571
+ messages: 1,
1572
+ targets: 1,
1573
+ syncId,
1574
+ }),
1575
+ () =>
1576
+ emitSyncProfileDuration(profile, sendStartedAt, {
1577
+ name: "rateless.sendRequestMoreSymbols",
1578
+ messages: 1,
1579
+ targets: 1,
1580
+ syncId,
1581
+ details: { rejected: true },
1582
+ }),
1583
+ );
1584
+ }
957
1585
 
958
1586
  return true;
959
1587
  } else if (message instanceof RequestMoreSymbols) {
960
- const obj = this.outgoingSyncProcesses.get(getSyncIdString(message));
1588
+ const syncId = getSyncIdString(message);
1589
+ const obj = this.outgoingSyncProcesses.get(syncId);
961
1590
  if (!obj) {
962
1591
  return true;
963
1592
  }
1593
+ const symbols = obj.next(message);
1594
+ const sendStartedAt = syncProfileStart(profile);
964
1595
  await this.properties.rpc.send(
965
1596
  new MoreSymbols({
966
1597
  lastSeqNo: message.lastSeqNo,
967
1598
  syncId: message.syncId,
968
- symbols: obj.next(message).map((x) => new SymbolSerialized(x)),
1599
+ symbols,
969
1600
  }),
970
1601
  {
971
1602
  mode: new SilentDelivery({ to: [context.from!], redundancy: 1 }),
972
- priority: 1,
1603
+ priority: SYNC_MESSAGE_PRIORITY,
973
1604
  },
974
1605
  );
1606
+ if (profile) {
1607
+ emitSyncProfileDuration(profile, sendStartedAt, {
1608
+ name: "rateless.sendMoreSymbols",
1609
+ messages: 1,
1610
+ symbols: symbols.length,
1611
+ targets: 1,
1612
+ syncId,
1613
+ });
1614
+ }
975
1615
  return true;
976
1616
  } else if (message instanceof RequestAll) {
977
1617
  const p = this.outgoingSyncProcesses.get(getSyncIdString(message));