@peerbit/document 12.3.4 → 12.3.5-3f16953
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.
- package/dist/benchmark/iterate-replicate-2.js +0 -13
- package/dist/benchmark/iterate-replicate-2.js.map +1 -1
- package/dist/benchmark/iterate-replicate.js +0 -13
- package/dist/benchmark/iterate-replicate.js.map +1 -1
- package/dist/benchmark/replication-network.d.ts +2 -0
- package/dist/benchmark/replication-network.d.ts.map +1 -0
- package/dist/benchmark/replication-network.js +872 -0
- package/dist/benchmark/replication-network.js.map +1 -0
- package/dist/benchmark/replication.js +0 -19
- package/dist/benchmark/replication.js.map +1 -1
- package/dist/src/program.d.ts.map +1 -1
- package/dist/src/program.js +1 -0
- package/dist/src/program.js.map +1 -1
- package/dist/src/resumable-iterator.d.ts +1 -0
- package/dist/src/resumable-iterator.d.ts.map +1 -1
- package/dist/src/resumable-iterator.js +29 -0
- package/dist/src/resumable-iterator.js.map +1 -1
- package/dist/src/search.d.ts.map +1 -1
- package/dist/src/search.js +167 -67
- package/dist/src/search.js.map +1 -1
- package/package.json +20 -20
- package/src/program.ts +1 -0
- package/src/resumable-iterator.ts +33 -0
- package/src/search.ts +271 -162
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
2
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
3
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
4
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
5
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
6
|
+
var _, done = false;
|
|
7
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
8
|
+
var context = {};
|
|
9
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
10
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
11
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
12
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
13
|
+
if (kind === "accessor") {
|
|
14
|
+
if (result === void 0) continue;
|
|
15
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
16
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
17
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
18
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
19
|
+
}
|
|
20
|
+
else if (_ = accept(result)) {
|
|
21
|
+
if (kind === "field") initializers.unshift(_);
|
|
22
|
+
else descriptor[key] = _;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
26
|
+
done = true;
|
|
27
|
+
};
|
|
28
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
29
|
+
var useValue = arguments.length > 2;
|
|
30
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
31
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
32
|
+
}
|
|
33
|
+
return useValue ? value : void 0;
|
|
34
|
+
};
|
|
35
|
+
import { field, option, variant } from "@dao-xyz/borsh";
|
|
36
|
+
import { create as createSimpleIndexer } from "@peerbit/indexer-simple";
|
|
37
|
+
import { Program } from "@peerbit/program";
|
|
38
|
+
import { TestSession } from "@peerbit/test-utils";
|
|
39
|
+
import { performance } from "node:perf_hooks";
|
|
40
|
+
import { v4 as uuid } from "uuid";
|
|
41
|
+
import { Documents } from "../src/program.js";
|
|
42
|
+
/**
|
|
43
|
+
* Run examples:
|
|
44
|
+
* node --loader ts-node/esm ./benchmark/replication-network.ts --nodes 5 --docs 20 --topology full --replicateFactor 1
|
|
45
|
+
* node --loader ts-node/esm ./benchmark/replication-network.ts --nodes 100 --docs 50 --topology random --degree 8 --replicateFactor 1 --mockCrypto true --fanoutJoinTimeoutMs 240000
|
|
46
|
+
*/
|
|
47
|
+
let Document = (() => {
|
|
48
|
+
let _classDecorators = [variant("bench_document")];
|
|
49
|
+
let _classDescriptor;
|
|
50
|
+
let _classExtraInitializers = [];
|
|
51
|
+
let _classThis;
|
|
52
|
+
let _id_decorators;
|
|
53
|
+
let _id_initializers = [];
|
|
54
|
+
let _id_extraInitializers = [];
|
|
55
|
+
let _payload_decorators;
|
|
56
|
+
let _payload_initializers = [];
|
|
57
|
+
let _payload_extraInitializers = [];
|
|
58
|
+
var Document = class {
|
|
59
|
+
static { _classThis = this; }
|
|
60
|
+
static {
|
|
61
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
62
|
+
_id_decorators = [field({ type: "string" })];
|
|
63
|
+
_payload_decorators = [field({ type: option("string") })];
|
|
64
|
+
__esDecorate(null, null, _id_decorators, { kind: "field", name: "id", static: false, private: false, access: { has: obj => "id" in obj, get: obj => obj.id, set: (obj, value) => { obj.id = value; } }, metadata: _metadata }, _id_initializers, _id_extraInitializers);
|
|
65
|
+
__esDecorate(null, null, _payload_decorators, { kind: "field", name: "payload", static: false, private: false, access: { has: obj => "payload" in obj, get: obj => obj.payload, set: (obj, value) => { obj.payload = value; } }, metadata: _metadata }, _payload_initializers, _payload_extraInitializers);
|
|
66
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
67
|
+
Document = _classThis = _classDescriptor.value;
|
|
68
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
69
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
70
|
+
}
|
|
71
|
+
id = __runInitializers(this, _id_initializers, void 0);
|
|
72
|
+
payload = (__runInitializers(this, _id_extraInitializers), __runInitializers(this, _payload_initializers, void 0));
|
|
73
|
+
constructor(properties) {
|
|
74
|
+
__runInitializers(this, _payload_extraInitializers);
|
|
75
|
+
this.id = properties?.id ?? "";
|
|
76
|
+
this.payload = properties?.payload;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
return Document = _classThis;
|
|
80
|
+
})();
|
|
81
|
+
let TestStore = (() => {
|
|
82
|
+
let _classDecorators = [variant("bench_documents_store")];
|
|
83
|
+
let _classDescriptor;
|
|
84
|
+
let _classExtraInitializers = [];
|
|
85
|
+
let _classThis;
|
|
86
|
+
let _classSuper = Program;
|
|
87
|
+
let _docs_decorators;
|
|
88
|
+
let _docs_initializers = [];
|
|
89
|
+
let _docs_extraInitializers = [];
|
|
90
|
+
var TestStore = class extends _classSuper {
|
|
91
|
+
static { _classThis = this; }
|
|
92
|
+
static {
|
|
93
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
94
|
+
_docs_decorators = [field({ type: Documents })];
|
|
95
|
+
__esDecorate(null, null, _docs_decorators, { kind: "field", name: "docs", static: false, private: false, access: { has: obj => "docs" in obj, get: obj => obj.docs, set: (obj, value) => { obj.docs = value; } }, metadata: _metadata }, _docs_initializers, _docs_extraInitializers);
|
|
96
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
97
|
+
TestStore = _classThis = _classDescriptor.value;
|
|
98
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
99
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
100
|
+
}
|
|
101
|
+
docs = __runInitializers(this, _docs_initializers, void 0);
|
|
102
|
+
constructor() {
|
|
103
|
+
super();
|
|
104
|
+
__runInitializers(this, _docs_extraInitializers);
|
|
105
|
+
this.docs = new Documents();
|
|
106
|
+
}
|
|
107
|
+
async open(options) {
|
|
108
|
+
await this.docs.open({ ...options, type: Document });
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
return TestStore = _classThis;
|
|
112
|
+
})();
|
|
113
|
+
const DEFAULTS = {
|
|
114
|
+
nodes: 5,
|
|
115
|
+
docs: 20,
|
|
116
|
+
warmup: 3,
|
|
117
|
+
topology: "full",
|
|
118
|
+
degree: 3,
|
|
119
|
+
replicateFactor: 1,
|
|
120
|
+
writer: 0,
|
|
121
|
+
concurrency: 1,
|
|
122
|
+
payloadBytes: 256,
|
|
123
|
+
timeoutMs: 20_000,
|
|
124
|
+
openTimeoutMs: 120_000,
|
|
125
|
+
seed: 1,
|
|
126
|
+
mockCrypto: true,
|
|
127
|
+
timeUntilRoleMaturity: 0,
|
|
128
|
+
fanoutJoinTimeoutMs: 180_000,
|
|
129
|
+
joinReqTimeoutMs: 500,
|
|
130
|
+
joinAttemptsPerRound: 2,
|
|
131
|
+
trackerCandidates: 8,
|
|
132
|
+
trackerQueryTimeoutMs: 500,
|
|
133
|
+
candidateShuffleTopK: 4,
|
|
134
|
+
candidateCooldownMs: 1_000,
|
|
135
|
+
bootstrapMaxPeers: 2,
|
|
136
|
+
trackerWarmupMs: 5_000,
|
|
137
|
+
admissionBatchSize: 25,
|
|
138
|
+
admissionPauseMs: 1_000,
|
|
139
|
+
failOnTimeout: true,
|
|
140
|
+
};
|
|
141
|
+
const parseBool = (value) => {
|
|
142
|
+
const normalized = value.trim().toLowerCase();
|
|
143
|
+
if (["1", "true", "yes", "y", "on"].includes(normalized))
|
|
144
|
+
return true;
|
|
145
|
+
if (["0", "false", "no", "n", "off"].includes(normalized))
|
|
146
|
+
return false;
|
|
147
|
+
throw new Error(`Invalid boolean '${value}'`);
|
|
148
|
+
};
|
|
149
|
+
const parseArgs = (argv) => {
|
|
150
|
+
const out = { ...DEFAULTS };
|
|
151
|
+
for (let i = 0; i < argv.length; i++) {
|
|
152
|
+
const arg = argv[i];
|
|
153
|
+
const next = argv[i + 1];
|
|
154
|
+
if (arg === "--help" || arg === "-h") {
|
|
155
|
+
console.log([
|
|
156
|
+
"replication-network benchmark",
|
|
157
|
+
"",
|
|
158
|
+
"Flags:",
|
|
159
|
+
" --nodes N number of peers (default: 5)",
|
|
160
|
+
" --docs N measured writes (default: 20)",
|
|
161
|
+
" --warmup N warmup writes before measuring (default: 3)",
|
|
162
|
+
" --topology full|line|random (default: full)",
|
|
163
|
+
" --degree N random-graph degree when --topology random (default: 3)",
|
|
164
|
+
" --replicateFactor X replication factor [0..1], where 1 = full-domain (default: 1)",
|
|
165
|
+
" --writer N writer peer index (default: 0)",
|
|
166
|
+
" --concurrency N in-flight puts (default: 1)",
|
|
167
|
+
" --payloadBytes N payload size in bytes (default: 256)",
|
|
168
|
+
" --timeoutMs N timeout per doc convergence (default: 20000)",
|
|
169
|
+
" --openTimeoutMs N timeout per peer open/bootstrap step (default: 120000)",
|
|
170
|
+
" --seed N random seed for random topology (default: 1)",
|
|
171
|
+
" --mockCrypto true|false in-memory crypto shortcut (default: true)",
|
|
172
|
+
" --timeUntilRoleMaturity N role maturity (ms) for opened stores (default: 0)",
|
|
173
|
+
" --fanoutJoinTimeoutMs N fanout join timeout used during bootstrap (default: 180000)",
|
|
174
|
+
" --joinReqTimeoutMs N timeout per JOIN_REQ attempt (default: 500)",
|
|
175
|
+
" --joinAttemptsPerRound N join attempts per retry round (default: 2)",
|
|
176
|
+
" --trackerCandidates N tracker candidates requested per query (default: 8)",
|
|
177
|
+
" --trackerQueryTimeoutMs N timeout for tracker reply (default: 500)",
|
|
178
|
+
" --candidateShuffleTopK N shuffled candidate window (default: 4)",
|
|
179
|
+
" --candidateCooldownMs N candidate cooldown after failures (default: 1000)",
|
|
180
|
+
" --bootstrapMaxPeers N bootstrap peers to keep dialed (default: 2)",
|
|
181
|
+
" --trackerWarmupMs N wait before opening stores in sparse topologies (default: 5000)",
|
|
182
|
+
" --admissionBatchSize N stores opened before pause (default: 25)",
|
|
183
|
+
" --admissionPauseMs N pause between admission batches (default: 1000)",
|
|
184
|
+
" --failOnTimeout true|false exit non-zero if any write does not converge (default: true)",
|
|
185
|
+
].join("\n"));
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
const consume = () => {
|
|
189
|
+
if (next == null || next.startsWith("--")) {
|
|
190
|
+
throw new Error(`Missing value for ${arg}`);
|
|
191
|
+
}
|
|
192
|
+
i += 1;
|
|
193
|
+
return next;
|
|
194
|
+
};
|
|
195
|
+
switch (arg) {
|
|
196
|
+
case "--nodes":
|
|
197
|
+
out.nodes = Number.parseInt(consume(), 10);
|
|
198
|
+
break;
|
|
199
|
+
case "--docs":
|
|
200
|
+
out.docs = Number.parseInt(consume(), 10);
|
|
201
|
+
break;
|
|
202
|
+
case "--warmup":
|
|
203
|
+
out.warmup = Number.parseInt(consume(), 10);
|
|
204
|
+
break;
|
|
205
|
+
case "--topology":
|
|
206
|
+
out.topology = consume();
|
|
207
|
+
break;
|
|
208
|
+
case "--degree":
|
|
209
|
+
out.degree = Number.parseInt(consume(), 10);
|
|
210
|
+
break;
|
|
211
|
+
case "--replicateFactor":
|
|
212
|
+
out.replicateFactor = Number.parseFloat(consume());
|
|
213
|
+
break;
|
|
214
|
+
case "--writer":
|
|
215
|
+
out.writer = Number.parseInt(consume(), 10);
|
|
216
|
+
break;
|
|
217
|
+
case "--concurrency":
|
|
218
|
+
out.concurrency = Number.parseInt(consume(), 10);
|
|
219
|
+
break;
|
|
220
|
+
case "--payloadBytes":
|
|
221
|
+
out.payloadBytes = Number.parseInt(consume(), 10);
|
|
222
|
+
break;
|
|
223
|
+
case "--timeoutMs":
|
|
224
|
+
out.timeoutMs = Number.parseInt(consume(), 10);
|
|
225
|
+
break;
|
|
226
|
+
case "--openTimeoutMs":
|
|
227
|
+
out.openTimeoutMs = Number.parseInt(consume(), 10);
|
|
228
|
+
break;
|
|
229
|
+
case "--seed":
|
|
230
|
+
out.seed = Number.parseInt(consume(), 10);
|
|
231
|
+
break;
|
|
232
|
+
case "--mockCrypto":
|
|
233
|
+
out.mockCrypto = parseBool(consume());
|
|
234
|
+
break;
|
|
235
|
+
case "--timeUntilRoleMaturity":
|
|
236
|
+
out.timeUntilRoleMaturity = Number.parseInt(consume(), 10);
|
|
237
|
+
break;
|
|
238
|
+
case "--failOnTimeout":
|
|
239
|
+
out.failOnTimeout = parseBool(consume());
|
|
240
|
+
break;
|
|
241
|
+
case "--fanoutJoinTimeoutMs":
|
|
242
|
+
out.fanoutJoinTimeoutMs = Number.parseInt(consume(), 10);
|
|
243
|
+
break;
|
|
244
|
+
case "--joinReqTimeoutMs":
|
|
245
|
+
out.joinReqTimeoutMs = Number.parseInt(consume(), 10);
|
|
246
|
+
break;
|
|
247
|
+
case "--joinAttemptsPerRound":
|
|
248
|
+
out.joinAttemptsPerRound = Number.parseInt(consume(), 10);
|
|
249
|
+
break;
|
|
250
|
+
case "--trackerCandidates":
|
|
251
|
+
out.trackerCandidates = Number.parseInt(consume(), 10);
|
|
252
|
+
break;
|
|
253
|
+
case "--trackerQueryTimeoutMs":
|
|
254
|
+
out.trackerQueryTimeoutMs = Number.parseInt(consume(), 10);
|
|
255
|
+
break;
|
|
256
|
+
case "--candidateShuffleTopK":
|
|
257
|
+
out.candidateShuffleTopK = Number.parseInt(consume(), 10);
|
|
258
|
+
break;
|
|
259
|
+
case "--candidateCooldownMs":
|
|
260
|
+
out.candidateCooldownMs = Number.parseInt(consume(), 10);
|
|
261
|
+
break;
|
|
262
|
+
case "--bootstrapMaxPeers":
|
|
263
|
+
out.bootstrapMaxPeers = Number.parseInt(consume(), 10);
|
|
264
|
+
break;
|
|
265
|
+
case "--trackerWarmupMs":
|
|
266
|
+
out.trackerWarmupMs = Number.parseInt(consume(), 10);
|
|
267
|
+
break;
|
|
268
|
+
case "--admissionBatchSize":
|
|
269
|
+
out.admissionBatchSize = Number.parseInt(consume(), 10);
|
|
270
|
+
break;
|
|
271
|
+
case "--admissionPauseMs":
|
|
272
|
+
out.admissionPauseMs = Number.parseInt(consume(), 10);
|
|
273
|
+
break;
|
|
274
|
+
default:
|
|
275
|
+
if (arg.startsWith("--")) {
|
|
276
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (!Number.isFinite(out.nodes) || out.nodes < 2) {
|
|
282
|
+
throw new Error(`Expected --nodes >= 2, got '${out.nodes}'`);
|
|
283
|
+
}
|
|
284
|
+
if (!Number.isFinite(out.docs) || out.docs < 1) {
|
|
285
|
+
throw new Error(`Expected --docs >= 1, got '${out.docs}'`);
|
|
286
|
+
}
|
|
287
|
+
if (!Number.isFinite(out.warmup) || out.warmup < 0) {
|
|
288
|
+
throw new Error(`Expected --warmup >= 0, got '${out.warmup}'`);
|
|
289
|
+
}
|
|
290
|
+
if (!["full", "line", "random"].includes(out.topology)) {
|
|
291
|
+
throw new Error(`Expected --topology to be full|line|random, got '${out.topology}'`);
|
|
292
|
+
}
|
|
293
|
+
if (!Number.isFinite(out.degree) || out.degree < 1) {
|
|
294
|
+
throw new Error(`Expected --degree >= 1, got '${out.degree}'`);
|
|
295
|
+
}
|
|
296
|
+
if (!Number.isFinite(out.replicateFactor) ||
|
|
297
|
+
out.replicateFactor < 0 ||
|
|
298
|
+
out.replicateFactor > 1) {
|
|
299
|
+
throw new Error(`Expected --replicateFactor in [0,1], got '${out.replicateFactor}'`);
|
|
300
|
+
}
|
|
301
|
+
if (!Number.isFinite(out.writer) || out.writer < 0 || out.writer >= out.nodes) {
|
|
302
|
+
throw new Error(`Expected --writer in [0, ${out.nodes - 1}], got '${out.writer}'`);
|
|
303
|
+
}
|
|
304
|
+
if (!Number.isFinite(out.concurrency) || out.concurrency < 1) {
|
|
305
|
+
throw new Error(`Expected --concurrency >= 1, got '${out.concurrency}'`);
|
|
306
|
+
}
|
|
307
|
+
if (!Number.isFinite(out.payloadBytes) || out.payloadBytes < 0) {
|
|
308
|
+
throw new Error(`Expected --payloadBytes >= 0, got '${out.payloadBytes}'`);
|
|
309
|
+
}
|
|
310
|
+
if (!Number.isFinite(out.timeoutMs) || out.timeoutMs < 1) {
|
|
311
|
+
throw new Error(`Expected --timeoutMs >= 1, got '${out.timeoutMs}'`);
|
|
312
|
+
}
|
|
313
|
+
if (!Number.isFinite(out.openTimeoutMs) || out.openTimeoutMs < 1) {
|
|
314
|
+
throw new Error(`Expected --openTimeoutMs >= 1, got '${out.openTimeoutMs}'`);
|
|
315
|
+
}
|
|
316
|
+
if (!Number.isFinite(out.timeUntilRoleMaturity) ||
|
|
317
|
+
out.timeUntilRoleMaturity < 0) {
|
|
318
|
+
throw new Error(`Expected --timeUntilRoleMaturity >= 0, got '${out.timeUntilRoleMaturity}'`);
|
|
319
|
+
}
|
|
320
|
+
if (!Number.isFinite(out.fanoutJoinTimeoutMs) || out.fanoutJoinTimeoutMs < 1) {
|
|
321
|
+
throw new Error(`Expected --fanoutJoinTimeoutMs >= 1, got '${out.fanoutJoinTimeoutMs}'`);
|
|
322
|
+
}
|
|
323
|
+
if (!Number.isFinite(out.joinReqTimeoutMs) || out.joinReqTimeoutMs < 1) {
|
|
324
|
+
throw new Error(`Expected --joinReqTimeoutMs >= 1, got '${out.joinReqTimeoutMs}'`);
|
|
325
|
+
}
|
|
326
|
+
if (!Number.isFinite(out.joinAttemptsPerRound) || out.joinAttemptsPerRound < 1) {
|
|
327
|
+
throw new Error(`Expected --joinAttemptsPerRound >= 1, got '${out.joinAttemptsPerRound}'`);
|
|
328
|
+
}
|
|
329
|
+
if (!Number.isFinite(out.trackerCandidates) || out.trackerCandidates < 1) {
|
|
330
|
+
throw new Error(`Expected --trackerCandidates >= 1, got '${out.trackerCandidates}'`);
|
|
331
|
+
}
|
|
332
|
+
if (!Number.isFinite(out.trackerQueryTimeoutMs) ||
|
|
333
|
+
out.trackerQueryTimeoutMs < 1) {
|
|
334
|
+
throw new Error(`Expected --trackerQueryTimeoutMs >= 1, got '${out.trackerQueryTimeoutMs}'`);
|
|
335
|
+
}
|
|
336
|
+
if (!Number.isFinite(out.candidateShuffleTopK) || out.candidateShuffleTopK < 0) {
|
|
337
|
+
throw new Error(`Expected --candidateShuffleTopK >= 0, got '${out.candidateShuffleTopK}'`);
|
|
338
|
+
}
|
|
339
|
+
if (!Number.isFinite(out.candidateCooldownMs) || out.candidateCooldownMs < 0) {
|
|
340
|
+
throw new Error(`Expected --candidateCooldownMs >= 0, got '${out.candidateCooldownMs}'`);
|
|
341
|
+
}
|
|
342
|
+
if (!Number.isFinite(out.bootstrapMaxPeers) || out.bootstrapMaxPeers < 1) {
|
|
343
|
+
throw new Error(`Expected --bootstrapMaxPeers >= 1, got '${out.bootstrapMaxPeers}'`);
|
|
344
|
+
}
|
|
345
|
+
if (!Number.isFinite(out.trackerWarmupMs) || out.trackerWarmupMs < 0) {
|
|
346
|
+
throw new Error(`Expected --trackerWarmupMs >= 0, got '${out.trackerWarmupMs}'`);
|
|
347
|
+
}
|
|
348
|
+
if (!Number.isFinite(out.admissionBatchSize) || out.admissionBatchSize < 1) {
|
|
349
|
+
throw new Error(`Expected --admissionBatchSize >= 1, got '${out.admissionBatchSize}'`);
|
|
350
|
+
}
|
|
351
|
+
if (!Number.isFinite(out.admissionPauseMs) || out.admissionPauseMs < 0) {
|
|
352
|
+
throw new Error(`Expected --admissionPauseMs >= 0, got '${out.admissionPauseMs}'`);
|
|
353
|
+
}
|
|
354
|
+
out.nodes = Math.floor(out.nodes);
|
|
355
|
+
out.docs = Math.floor(out.docs);
|
|
356
|
+
out.warmup = Math.floor(out.warmup);
|
|
357
|
+
out.degree = Math.floor(out.degree);
|
|
358
|
+
out.writer = Math.floor(out.writer);
|
|
359
|
+
out.concurrency = Math.floor(out.concurrency);
|
|
360
|
+
out.payloadBytes = Math.floor(out.payloadBytes);
|
|
361
|
+
out.timeoutMs = Math.floor(out.timeoutMs);
|
|
362
|
+
out.openTimeoutMs = Math.floor(out.openTimeoutMs);
|
|
363
|
+
out.seed = Math.floor(out.seed);
|
|
364
|
+
out.timeUntilRoleMaturity = Math.floor(out.timeUntilRoleMaturity);
|
|
365
|
+
out.fanoutJoinTimeoutMs = Math.floor(out.fanoutJoinTimeoutMs);
|
|
366
|
+
out.joinReqTimeoutMs = Math.floor(out.joinReqTimeoutMs);
|
|
367
|
+
out.joinAttemptsPerRound = Math.floor(out.joinAttemptsPerRound);
|
|
368
|
+
out.trackerCandidates = Math.floor(out.trackerCandidates);
|
|
369
|
+
out.trackerQueryTimeoutMs = Math.floor(out.trackerQueryTimeoutMs);
|
|
370
|
+
out.candidateShuffleTopK = Math.floor(out.candidateShuffleTopK);
|
|
371
|
+
out.candidateCooldownMs = Math.floor(out.candidateCooldownMs);
|
|
372
|
+
out.bootstrapMaxPeers = Math.floor(out.bootstrapMaxPeers);
|
|
373
|
+
out.trackerWarmupMs = Math.floor(out.trackerWarmupMs);
|
|
374
|
+
out.admissionBatchSize = Math.floor(out.admissionBatchSize);
|
|
375
|
+
out.admissionPauseMs = Math.floor(out.admissionPauseMs);
|
|
376
|
+
return out;
|
|
377
|
+
};
|
|
378
|
+
const percentile = (values, p) => {
|
|
379
|
+
if (values.length === 0)
|
|
380
|
+
return Number.NaN;
|
|
381
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
382
|
+
const pos = Math.min(sorted.length - 1, Math.max(0, Math.floor(p * (sorted.length - 1))));
|
|
383
|
+
return sorted[pos];
|
|
384
|
+
};
|
|
385
|
+
const stats = (values) => {
|
|
386
|
+
if (values.length === 0) {
|
|
387
|
+
return {
|
|
388
|
+
count: 0,
|
|
389
|
+
min: Number.NaN,
|
|
390
|
+
max: Number.NaN,
|
|
391
|
+
mean: Number.NaN,
|
|
392
|
+
p50: Number.NaN,
|
|
393
|
+
p95: Number.NaN,
|
|
394
|
+
p99: Number.NaN,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
let sum = 0;
|
|
398
|
+
let min = Number.POSITIVE_INFINITY;
|
|
399
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
400
|
+
for (const v of values) {
|
|
401
|
+
sum += v;
|
|
402
|
+
min = Math.min(min, v);
|
|
403
|
+
max = Math.max(max, v);
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
count: values.length,
|
|
407
|
+
min,
|
|
408
|
+
max,
|
|
409
|
+
mean: sum / values.length,
|
|
410
|
+
p50: percentile(values, 0.5),
|
|
411
|
+
p95: percentile(values, 0.95),
|
|
412
|
+
p99: percentile(values, 0.99),
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
const formatMs = (value) => (Number.isFinite(value) ? `${value.toFixed(2)} ms` : "n/a");
|
|
416
|
+
const formatRate = (value) => Number.isFinite(value) ? `${value.toFixed(2)} /s` : "n/a";
|
|
417
|
+
const buildPayload = (payloadBytes) => {
|
|
418
|
+
if (payloadBytes <= 0)
|
|
419
|
+
return "";
|
|
420
|
+
return "x".repeat(payloadBytes);
|
|
421
|
+
};
|
|
422
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
423
|
+
setTimeout(resolve, ms);
|
|
424
|
+
});
|
|
425
|
+
const withTimeout = async (promise, timeoutMs, label) => {
|
|
426
|
+
let timeoutId;
|
|
427
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
428
|
+
timeoutId = setTimeout(() => {
|
|
429
|
+
reject(new Error(`${label} timed out after ${timeoutMs} ms`));
|
|
430
|
+
}, timeoutMs);
|
|
431
|
+
});
|
|
432
|
+
try {
|
|
433
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
if (timeoutId)
|
|
437
|
+
clearTimeout(timeoutId);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const buildOpenOrder = (args, bootstrapIndices) => {
|
|
441
|
+
const seen = new Set();
|
|
442
|
+
const out = [];
|
|
443
|
+
const push = (i) => {
|
|
444
|
+
if (i < 0 || i >= args.nodes)
|
|
445
|
+
return;
|
|
446
|
+
if (seen.has(i))
|
|
447
|
+
return;
|
|
448
|
+
seen.add(i);
|
|
449
|
+
out.push(i);
|
|
450
|
+
};
|
|
451
|
+
push(args.writer);
|
|
452
|
+
for (const i of bootstrapIndices)
|
|
453
|
+
push(i);
|
|
454
|
+
for (let i = 0; i < args.nodes; i++)
|
|
455
|
+
push(i);
|
|
456
|
+
return out;
|
|
457
|
+
};
|
|
458
|
+
const configureTopology = async (session, args) => {
|
|
459
|
+
if (args.topology === "full") {
|
|
460
|
+
await session.connect();
|
|
461
|
+
const edgeCount = Math.floor((args.nodes * (args.nodes - 1)) / 2);
|
|
462
|
+
return { edgeCount, topologyNote: "fully connected" };
|
|
463
|
+
}
|
|
464
|
+
if (args.topology === "line") {
|
|
465
|
+
const peers = session.peers;
|
|
466
|
+
const groups = [];
|
|
467
|
+
for (let i = 0; i < peers.length - 1; i++) {
|
|
468
|
+
groups.push([peers[i], peers[i + 1]]);
|
|
469
|
+
}
|
|
470
|
+
await session.connect(groups);
|
|
471
|
+
return {
|
|
472
|
+
edgeCount: Math.max(0, args.nodes - 1),
|
|
473
|
+
topologyNote: "line (pairwise groups)",
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const requestedDegree = Math.min(args.degree, Math.max(1, args.nodes - 1));
|
|
477
|
+
const adjacency = await session.connectRandomGraph({
|
|
478
|
+
degree: requestedDegree,
|
|
479
|
+
seed: args.seed,
|
|
480
|
+
});
|
|
481
|
+
let edgeCount = 0;
|
|
482
|
+
for (const graph of adjacency) {
|
|
483
|
+
for (let i = 0; i < graph.length; i++) {
|
|
484
|
+
for (const j of graph[i]) {
|
|
485
|
+
if (j > i)
|
|
486
|
+
edgeCount += 1;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
edgeCount,
|
|
492
|
+
topologyNote: `random bounded-degree (degree<=${requestedDegree})`,
|
|
493
|
+
};
|
|
494
|
+
};
|
|
495
|
+
const main = async () => {
|
|
496
|
+
const args = parseArgs(process.argv.slice(2));
|
|
497
|
+
const payloadTemplate = buildPayload(args.payloadBytes);
|
|
498
|
+
const targets = new Set();
|
|
499
|
+
for (let i = 0; i < args.nodes; i++) {
|
|
500
|
+
if (i !== args.writer)
|
|
501
|
+
targets.add(i);
|
|
502
|
+
}
|
|
503
|
+
console.log("Starting replication-network benchmark");
|
|
504
|
+
console.log(JSON.stringify({
|
|
505
|
+
nodes: args.nodes,
|
|
506
|
+
docs: args.docs,
|
|
507
|
+
warmup: args.warmup,
|
|
508
|
+
topology: args.topology,
|
|
509
|
+
degree: args.degree,
|
|
510
|
+
replicateFactor: args.replicateFactor,
|
|
511
|
+
writer: args.writer,
|
|
512
|
+
concurrency: args.concurrency,
|
|
513
|
+
payloadBytes: args.payloadBytes,
|
|
514
|
+
timeoutMs: args.timeoutMs,
|
|
515
|
+
openTimeoutMs: args.openTimeoutMs,
|
|
516
|
+
seed: args.seed,
|
|
517
|
+
mockCrypto: args.mockCrypto,
|
|
518
|
+
timeUntilRoleMaturity: args.timeUntilRoleMaturity,
|
|
519
|
+
fanoutJoinTimeoutMs: args.fanoutJoinTimeoutMs,
|
|
520
|
+
joinReqTimeoutMs: args.joinReqTimeoutMs,
|
|
521
|
+
joinAttemptsPerRound: args.joinAttemptsPerRound,
|
|
522
|
+
trackerCandidates: args.trackerCandidates,
|
|
523
|
+
trackerQueryTimeoutMs: args.trackerQueryTimeoutMs,
|
|
524
|
+
candidateShuffleTopK: args.candidateShuffleTopK,
|
|
525
|
+
candidateCooldownMs: args.candidateCooldownMs,
|
|
526
|
+
bootstrapMaxPeers: args.bootstrapMaxPeers,
|
|
527
|
+
trackerWarmupMs: args.trackerWarmupMs,
|
|
528
|
+
admissionBatchSize: args.admissionBatchSize,
|
|
529
|
+
admissionPauseMs: args.admissionPauseMs,
|
|
530
|
+
failOnTimeout: args.failOnTimeout,
|
|
531
|
+
}, null, 2));
|
|
532
|
+
let session;
|
|
533
|
+
let stores = [];
|
|
534
|
+
const listeners = [];
|
|
535
|
+
const trackers = new Map();
|
|
536
|
+
const perNodeReceivedCount = new Array(args.nodes).fill(0);
|
|
537
|
+
try {
|
|
538
|
+
session = await TestSession.disconnectedInMemory(args.nodes, {
|
|
539
|
+
mockCrypto: args.mockCrypto,
|
|
540
|
+
seed: args.seed,
|
|
541
|
+
indexer: createSimpleIndexer,
|
|
542
|
+
});
|
|
543
|
+
const bootstrapCount = Math.min(8, args.nodes);
|
|
544
|
+
const bootstrapIndices = Array.from({ length: bootstrapCount }, (_, i) => i);
|
|
545
|
+
const bootstrapAddrs = bootstrapIndices.flatMap((index) => (session.peers[index]?.getMultiaddrs?.() ?? [])
|
|
546
|
+
.slice(0, 1)
|
|
547
|
+
.map((ma) => ma?.toString?.())
|
|
548
|
+
.filter((x) => typeof x === "string" && x.length > 0));
|
|
549
|
+
if (bootstrapAddrs.length > 0) {
|
|
550
|
+
console.log(`bootstraps selected: ${bootstrapAddrs.length} peers`);
|
|
551
|
+
}
|
|
552
|
+
for (const peer of session.peers) {
|
|
553
|
+
const fanout = peer?.services?.fanout;
|
|
554
|
+
try {
|
|
555
|
+
fanout?.setBootstraps?.(bootstrapAddrs);
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
// ignore
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const topologyInfo = await configureTopology(session, args);
|
|
562
|
+
console.log(`Connected topology: ${topologyInfo.topologyNote}, edges=${topologyInfo.edgeCount}`);
|
|
563
|
+
if (bootstrapAddrs.length > 0) {
|
|
564
|
+
console.log(`join bootstraps configured: ${bootstrapAddrs.length} peers`);
|
|
565
|
+
}
|
|
566
|
+
for (const peer of session.peers) {
|
|
567
|
+
const pubsub = peer?.services?.pubsub;
|
|
568
|
+
if (!pubsub)
|
|
569
|
+
continue;
|
|
570
|
+
pubsub.fanoutJoinOptions = {
|
|
571
|
+
...(pubsub.fanoutJoinOptions ?? {}),
|
|
572
|
+
timeoutMs: args.fanoutJoinTimeoutMs,
|
|
573
|
+
joinReqTimeoutMs: args.joinReqTimeoutMs,
|
|
574
|
+
joinAttemptsPerRound: args.joinAttemptsPerRound,
|
|
575
|
+
trackerCandidates: args.trackerCandidates,
|
|
576
|
+
trackerQueryTimeoutMs: args.trackerQueryTimeoutMs,
|
|
577
|
+
candidateShuffleTopK: args.candidateShuffleTopK,
|
|
578
|
+
candidateCooldownMs: args.candidateCooldownMs,
|
|
579
|
+
bootstrap: bootstrapAddrs,
|
|
580
|
+
bootstrapMaxPeers: Math.min(bootstrapAddrs.length, args.bootstrapMaxPeers),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
await Promise.all(bootstrapIndices.map(async (index) => {
|
|
584
|
+
try {
|
|
585
|
+
await session.peers[index]?.services?.pubsub?.hostShardRootsNow?.();
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
// ignore
|
|
589
|
+
}
|
|
590
|
+
}));
|
|
591
|
+
if (args.topology !== "full" && args.trackerWarmupMs > 0) {
|
|
592
|
+
console.log(`tracker warmup: ${args.trackerWarmupMs} ms`);
|
|
593
|
+
await sleep(args.trackerWarmupMs);
|
|
594
|
+
}
|
|
595
|
+
let address;
|
|
596
|
+
const openOrder = buildOpenOrder(args, bootstrapIndices);
|
|
597
|
+
let openCount = 0;
|
|
598
|
+
for (const i of openOrder) {
|
|
599
|
+
const peer = session.peers[i];
|
|
600
|
+
const openArgs = {
|
|
601
|
+
replicate: { factor: args.replicateFactor },
|
|
602
|
+
timeUntilRoleMaturity: args.timeUntilRoleMaturity,
|
|
603
|
+
};
|
|
604
|
+
let store;
|
|
605
|
+
const maxAttempts = 3;
|
|
606
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
607
|
+
try {
|
|
608
|
+
store =
|
|
609
|
+
address == null
|
|
610
|
+
? await withTimeout(peer.open(new TestStore(), { args: openArgs }), args.openTimeoutMs, `open peer=${i} new-store`)
|
|
611
|
+
: await withTimeout(peer.open(address, { args: openArgs }), args.openTimeoutMs, `open peer=${i} address=${address}`);
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
if (attempt >= maxAttempts) {
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
const waitMs = Math.min(5_000, attempt * 1_000);
|
|
619
|
+
console.log(`open retry peer=${i} attempt=${attempt}/${maxAttempts} wait=${waitMs}ms`);
|
|
620
|
+
await sleep(waitMs);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (!store) {
|
|
624
|
+
throw new Error(`Failed to open store on peer ${i}`);
|
|
625
|
+
}
|
|
626
|
+
address = store.address;
|
|
627
|
+
stores[i] = store;
|
|
628
|
+
openCount += 1;
|
|
629
|
+
if (openCount === 1 ||
|
|
630
|
+
openCount === args.nodes ||
|
|
631
|
+
openCount % Math.max(1, Math.floor(args.nodes / 20)) === 0) {
|
|
632
|
+
console.log(`opened stores ${openCount}/${args.nodes}`);
|
|
633
|
+
}
|
|
634
|
+
if (args.admissionBatchSize > 0 &&
|
|
635
|
+
openCount < args.nodes &&
|
|
636
|
+
openCount % args.admissionBatchSize === 0 &&
|
|
637
|
+
args.admissionPauseMs > 0) {
|
|
638
|
+
console.log(`admission pause: ${args.admissionPauseMs} ms`);
|
|
639
|
+
await sleep(args.admissionPauseMs);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
await sleep(25);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const writerStore = stores[args.writer];
|
|
646
|
+
const settle = (index, docId) => {
|
|
647
|
+
const tracker = trackers.get(docId);
|
|
648
|
+
if (!tracker)
|
|
649
|
+
return;
|
|
650
|
+
if (!tracker.pendingTargets.has(index))
|
|
651
|
+
return;
|
|
652
|
+
tracker.pendingTargets.delete(index);
|
|
653
|
+
perNodeReceivedCount[index] += 1;
|
|
654
|
+
if (tracker.firstRemoteMs == null) {
|
|
655
|
+
tracker.firstRemoteMs = performance.now() - tracker.startMs;
|
|
656
|
+
}
|
|
657
|
+
if (tracker.pendingTargets.size === 0) {
|
|
658
|
+
clearTimeout(tracker.timer);
|
|
659
|
+
const latency = {
|
|
660
|
+
id: docId,
|
|
661
|
+
firstRemoteMs: tracker.firstRemoteMs,
|
|
662
|
+
convergedMs: performance.now() - tracker.startMs,
|
|
663
|
+
timedOut: false,
|
|
664
|
+
targetsReached: tracker.targetsTotal,
|
|
665
|
+
targetsTotal: tracker.targetsTotal,
|
|
666
|
+
};
|
|
667
|
+
trackers.delete(docId);
|
|
668
|
+
tracker.resolve(latency);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
stores.forEach((store, index) => {
|
|
672
|
+
if (!store)
|
|
673
|
+
return;
|
|
674
|
+
const listener = (event) => {
|
|
675
|
+
for (const added of event.detail?.added ?? []) {
|
|
676
|
+
if (typeof added?.id === "string") {
|
|
677
|
+
settle(index, added.id);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
store.docs.events.addEventListener("change", listener);
|
|
682
|
+
listeners.push(() => store.docs.events.removeEventListener("change", listener));
|
|
683
|
+
});
|
|
684
|
+
const measureOne = async (kind, seq) => {
|
|
685
|
+
const id = uuid();
|
|
686
|
+
const payload = payloadTemplate.length > 0 ? `${payloadTemplate}-${seq}` : undefined;
|
|
687
|
+
const doc = new Document({ id, payload });
|
|
688
|
+
const startMs = performance.now();
|
|
689
|
+
const pendingTargets = new Set(targets);
|
|
690
|
+
if (pendingTargets.size === 0) {
|
|
691
|
+
await writerStore.docs.put(doc, { unique: true });
|
|
692
|
+
return {
|
|
693
|
+
id,
|
|
694
|
+
firstRemoteMs: 0,
|
|
695
|
+
convergedMs: 0,
|
|
696
|
+
timedOut: false,
|
|
697
|
+
targetsReached: 0,
|
|
698
|
+
targetsTotal: 0,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
const done = new Promise((resolve) => {
|
|
702
|
+
const timer = setTimeout(() => {
|
|
703
|
+
const remaining = pendingTargets.size;
|
|
704
|
+
const reached = targets.size - remaining;
|
|
705
|
+
const latency = {
|
|
706
|
+
id,
|
|
707
|
+
firstRemoteMs: reached > 0 ? performance.now() - startMs : null,
|
|
708
|
+
convergedMs: null,
|
|
709
|
+
timedOut: true,
|
|
710
|
+
targetsReached: reached,
|
|
711
|
+
targetsTotal: targets.size,
|
|
712
|
+
};
|
|
713
|
+
trackers.delete(id);
|
|
714
|
+
resolve(latency);
|
|
715
|
+
}, args.timeoutMs);
|
|
716
|
+
trackers.set(id, {
|
|
717
|
+
startMs,
|
|
718
|
+
pendingTargets,
|
|
719
|
+
targetsTotal: targets.size,
|
|
720
|
+
firstRemoteMs: null,
|
|
721
|
+
resolve,
|
|
722
|
+
timer,
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
await writerStore.docs.put(doc, { unique: true });
|
|
726
|
+
const result = await done;
|
|
727
|
+
if (kind === "warmup") {
|
|
728
|
+
const warmupStatus = result.timedOut ? "timeout" : "ok";
|
|
729
|
+
console.log(`warmup ${seq + 1}/${args.warmup}: ${warmupStatus}, reached=${result.targetsReached}/${result.targetsTotal}`);
|
|
730
|
+
}
|
|
731
|
+
return result;
|
|
732
|
+
};
|
|
733
|
+
for (let i = 0; i < args.warmup; i++) {
|
|
734
|
+
await measureOne("warmup", i);
|
|
735
|
+
}
|
|
736
|
+
const measured = [];
|
|
737
|
+
const inFlight = new Set();
|
|
738
|
+
let started = 0;
|
|
739
|
+
const benchmarkStart = performance.now();
|
|
740
|
+
const startOne = (index) => {
|
|
741
|
+
const p = measureOne("measure", index)
|
|
742
|
+
.then((result) => {
|
|
743
|
+
measured.push(result);
|
|
744
|
+
})
|
|
745
|
+
.finally(() => {
|
|
746
|
+
inFlight.delete(p);
|
|
747
|
+
});
|
|
748
|
+
inFlight.add(p);
|
|
749
|
+
};
|
|
750
|
+
while (started < args.docs) {
|
|
751
|
+
while (started < args.docs && inFlight.size < args.concurrency) {
|
|
752
|
+
startOne(started);
|
|
753
|
+
started += 1;
|
|
754
|
+
}
|
|
755
|
+
if (inFlight.size > 0) {
|
|
756
|
+
await Promise.race(inFlight);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
await Promise.all(inFlight);
|
|
760
|
+
const benchmarkEnd = performance.now();
|
|
761
|
+
const firstRemoteValues = measured
|
|
762
|
+
.filter((x) => x.firstRemoteMs != null && !x.timedOut)
|
|
763
|
+
.map((x) => x.firstRemoteMs);
|
|
764
|
+
const convergedValues = measured
|
|
765
|
+
.filter((x) => x.convergedMs != null && !x.timedOut)
|
|
766
|
+
.map((x) => x.convergedMs);
|
|
767
|
+
const timeouts = measured.filter((x) => x.timedOut);
|
|
768
|
+
const timeoutCount = timeouts.length;
|
|
769
|
+
const successCount = measured.length - timeoutCount;
|
|
770
|
+
const durationSeconds = (benchmarkEnd - benchmarkStart) / 1000;
|
|
771
|
+
const docsPerSecond = measured.length / Math.max(1e-9, durationSeconds);
|
|
772
|
+
const deliveredCopies = measured.reduce((sum, x) => sum + x.targetsReached, 0);
|
|
773
|
+
const copyDeliveriesPerSecond = deliveredCopies / Math.max(1e-9, durationSeconds);
|
|
774
|
+
const bytesReplicated = deliveredCopies * Math.max(0, args.payloadBytes);
|
|
775
|
+
const bytesPerSecond = bytesReplicated / Math.max(1e-9, durationSeconds);
|
|
776
|
+
const firstStats = stats(firstRemoteValues);
|
|
777
|
+
const convergeStats = stats(convergedValues);
|
|
778
|
+
console.log("");
|
|
779
|
+
console.log("Results");
|
|
780
|
+
console.log(` measured docs: ${measured.length}`);
|
|
781
|
+
console.log(` succeeded: ${successCount}`);
|
|
782
|
+
console.log(` timed out: ${timeoutCount}`);
|
|
783
|
+
console.log(` duration: ${(benchmarkEnd - benchmarkStart).toFixed(2)} ms`);
|
|
784
|
+
console.log(` throughput (docs): ${formatRate(docsPerSecond)}`);
|
|
785
|
+
console.log(` throughput (replicated copies): ${formatRate(copyDeliveriesPerSecond)}`);
|
|
786
|
+
console.log(` throughput (payload bytes only): ${formatRate(bytesPerSecond)}`);
|
|
787
|
+
console.log("");
|
|
788
|
+
console.log("Latency to first remote replica");
|
|
789
|
+
console.log(` count: ${firstStats.count}`);
|
|
790
|
+
console.log(` min: ${formatMs(firstStats.min)}`);
|
|
791
|
+
console.log(` mean: ${formatMs(firstStats.mean)}`);
|
|
792
|
+
console.log(` p50: ${formatMs(firstStats.p50)}`);
|
|
793
|
+
console.log(` p95: ${formatMs(firstStats.p95)}`);
|
|
794
|
+
console.log(` p99: ${formatMs(firstStats.p99)}`);
|
|
795
|
+
console.log(` max: ${formatMs(firstStats.max)}`);
|
|
796
|
+
console.log("");
|
|
797
|
+
console.log("Latency to full convergence (all target peers)");
|
|
798
|
+
console.log(` count: ${convergeStats.count}`);
|
|
799
|
+
console.log(` min: ${formatMs(convergeStats.min)}`);
|
|
800
|
+
console.log(` mean: ${formatMs(convergeStats.mean)}`);
|
|
801
|
+
console.log(` p50: ${formatMs(convergeStats.p50)}`);
|
|
802
|
+
console.log(` p95: ${formatMs(convergeStats.p95)}`);
|
|
803
|
+
console.log(` p99: ${formatMs(convergeStats.p99)}`);
|
|
804
|
+
console.log(` max: ${formatMs(convergeStats.max)}`);
|
|
805
|
+
console.log("");
|
|
806
|
+
const remoteCounts = perNodeReceivedCount.filter((_, i) => i !== args.writer);
|
|
807
|
+
const remoteMin = remoteCounts.length > 0 ? Math.min(...remoteCounts) : 0;
|
|
808
|
+
const remoteMax = remoteCounts.length > 0 ? Math.max(...remoteCounts) : 0;
|
|
809
|
+
const remoteAvg = remoteCounts.length > 0
|
|
810
|
+
? remoteCounts.reduce((a, b) => a + b, 0) / remoteCounts.length
|
|
811
|
+
: 0;
|
|
812
|
+
console.log("Per-remote-node received docs");
|
|
813
|
+
console.log(` min: ${remoteMin}`);
|
|
814
|
+
console.log(` mean: ${remoteAvg.toFixed(2)}`);
|
|
815
|
+
console.log(` max: ${remoteMax}`);
|
|
816
|
+
if (timeoutCount > 0) {
|
|
817
|
+
console.log("");
|
|
818
|
+
console.log("Timeout samples");
|
|
819
|
+
for (const sample of timeouts.slice(0, 10)) {
|
|
820
|
+
console.log(` id=${sample.id} reached=${sample.targetsReached}/${sample.targetsTotal}`);
|
|
821
|
+
}
|
|
822
|
+
if (timeouts.length > 10) {
|
|
823
|
+
console.log(` ... ${timeouts.length - 10} more`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (args.failOnTimeout && timeoutCount > 0) {
|
|
827
|
+
throw new Error(`Benchmark had ${timeoutCount} timeouts out of ${measured.length} measured docs`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
finally {
|
|
831
|
+
for (const cleanup of listeners) {
|
|
832
|
+
try {
|
|
833
|
+
cleanup();
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
// ignore cleanup errors in benchmark mode
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
for (const tracker of trackers.values()) {
|
|
840
|
+
clearTimeout(tracker.timer);
|
|
841
|
+
}
|
|
842
|
+
trackers.clear();
|
|
843
|
+
await Promise.all(stores.map(async (store) => {
|
|
844
|
+
if (!store)
|
|
845
|
+
return;
|
|
846
|
+
try {
|
|
847
|
+
await store.close();
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
// ignore close errors in benchmark mode
|
|
851
|
+
}
|
|
852
|
+
}));
|
|
853
|
+
stores = [];
|
|
854
|
+
if (session) {
|
|
855
|
+
try {
|
|
856
|
+
await session.stop();
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
// ignore stop errors in benchmark mode
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
try {
|
|
865
|
+
await main();
|
|
866
|
+
process.exit(0);
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
console.error(error);
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
//# sourceMappingURL=replication-network.js.map
|