ferrings 0.1.0
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/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +428 -0
- package/benchmark/compare.js +178 -0
- package/benchmark/first-slice.js +232 -0
- package/benchmark/high-concurrency.js +119 -0
- package/benchmark/syscalls.js +514 -0
- package/benchmark/tcp-echo.js +442 -0
- package/bin/ferrings.js +503 -0
- package/examples/http-fixed.js +24 -0
- package/examples/tcp-echo.js +29 -0
- package/ferrings.linux-x64-gnu.node +0 -0
- package/index.d.ts +70 -0
- package/index.js +11 -0
- package/native.d.ts +213 -0
- package/native.js +594 -0
- package/package.json +95 -0
- package/tcp-transport.js +340 -0
- package/zcrx-smoke.js +476 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const net = require('node:net');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { performance } = require('node:perf_hooks');
|
|
7
|
+
const {
|
|
8
|
+
UringTcpEchoServer,
|
|
9
|
+
UringTcpServer,
|
|
10
|
+
capabilities,
|
|
11
|
+
createTcpServer
|
|
12
|
+
} = require('../');
|
|
13
|
+
|
|
14
|
+
const REQUEST = Buffer.from('ping');
|
|
15
|
+
const RESPONSE = Buffer.from('pong');
|
|
16
|
+
const DURATION_MS = Number(process.env.DURATION_MS || 5000);
|
|
17
|
+
const CONCURRENCY = Number(process.env.CONCURRENCY || 128);
|
|
18
|
+
const QUEUE_DEPTH = Number(process.env.QUEUE_DEPTH || 256);
|
|
19
|
+
const BUNDLE_REQUEST_SIZE = Number(process.env.BUNDLE_REQUEST_SIZE || 4096);
|
|
20
|
+
const BUNDLE_REQUEST = payload(BUNDLE_REQUEST_SIZE);
|
|
21
|
+
const CAPS = capabilities();
|
|
22
|
+
const REPORT_PATH = process.env.REPORT_PATH;
|
|
23
|
+
|
|
24
|
+
function payload(size) {
|
|
25
|
+
const data = Buffer.alloc(size);
|
|
26
|
+
for (let index = 0; index < data.length; index += 1) {
|
|
27
|
+
data[index] = index % 251;
|
|
28
|
+
}
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function echoOnce(port, request = REQUEST, expected = RESPONSE) {
|
|
33
|
+
const startedAt = performance.now();
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const socket = net.createConnection({ host: '127.0.0.1', port }, () => {
|
|
36
|
+
socket.write(request);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
let body = Buffer.alloc(0);
|
|
40
|
+
socket.on('data', (chunk) => {
|
|
41
|
+
body = Buffer.concat([body, chunk]);
|
|
42
|
+
if (body.length >= expected.length) {
|
|
43
|
+
socket.end();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
socket.on('end', () => {
|
|
47
|
+
if (!body.equals(expected)) {
|
|
48
|
+
reject(new Error(`unexpected response ${body.toString('hex')}`));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
resolve(performance.now() - startedAt);
|
|
52
|
+
});
|
|
53
|
+
socket.on('error', reject);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function runLoad(port, request = REQUEST, expected = RESPONSE) {
|
|
58
|
+
let completed = 0;
|
|
59
|
+
let stopped = false;
|
|
60
|
+
const latencies = [];
|
|
61
|
+
const endAt = performance.now() + DURATION_MS;
|
|
62
|
+
|
|
63
|
+
async function worker() {
|
|
64
|
+
while (!stopped) {
|
|
65
|
+
latencies.push(await echoOnce(port, request, expected));
|
|
66
|
+
completed += 1;
|
|
67
|
+
if (performance.now() >= endAt) stopped = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await Promise.all(Array.from({ length: CONCURRENCY }, worker));
|
|
72
|
+
latencies.sort((a, b) => a - b);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
requests: completed,
|
|
76
|
+
rps: Math.round((completed * 1000) / DURATION_MS),
|
|
77
|
+
p50Ms: percentile(latencies, 0.5),
|
|
78
|
+
p95Ms: percentile(latencies, 0.95),
|
|
79
|
+
p99Ms: percentile(latencies, 0.99)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function withNodeServer() {
|
|
84
|
+
const server = net.createServer((socket) => {
|
|
85
|
+
socket.once('data', () => {
|
|
86
|
+
socket.end(RESPONSE);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
91
|
+
const port = server.address().port;
|
|
92
|
+
try {
|
|
93
|
+
return await runLoad(port);
|
|
94
|
+
} finally {
|
|
95
|
+
await new Promise((resolve) => server.close(resolve));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function withUringNativeEchoServer(options = {}, request = REQUEST) {
|
|
100
|
+
const server = new UringTcpEchoServer({
|
|
101
|
+
host: '127.0.0.1',
|
|
102
|
+
port: 0,
|
|
103
|
+
queueDepth: QUEUE_DEPTH,
|
|
104
|
+
bufferCount: 4096,
|
|
105
|
+
bufferSize: 2048,
|
|
106
|
+
...options
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const info = server.start();
|
|
110
|
+
try {
|
|
111
|
+
return {
|
|
112
|
+
...(await runLoad(info.port, request, request)),
|
|
113
|
+
serverInfo: summarizeServerInfo(server.info())
|
|
114
|
+
};
|
|
115
|
+
} finally {
|
|
116
|
+
server.stop();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function withUringServer(options = {}) {
|
|
121
|
+
const server = new UringTcpServer({
|
|
122
|
+
host: '127.0.0.1',
|
|
123
|
+
port: 0,
|
|
124
|
+
queueDepth: QUEUE_DEPTH,
|
|
125
|
+
bufferCount: 4096,
|
|
126
|
+
bufferSize: 2048,
|
|
127
|
+
...options
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const info = server.start((event) => {
|
|
131
|
+
if (event.eventType === 'data') {
|
|
132
|
+
server.sendAndClose(event.connectionId, RESPONSE);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
return {
|
|
138
|
+
...(await runLoad(info.port)),
|
|
139
|
+
serverInfo: summarizeServerInfo(server.info())
|
|
140
|
+
};
|
|
141
|
+
} finally {
|
|
142
|
+
server.stop();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function withUringBatchServer(options = {}) {
|
|
147
|
+
const server = new UringTcpServer({
|
|
148
|
+
host: '127.0.0.1',
|
|
149
|
+
port: 0,
|
|
150
|
+
queueDepth: QUEUE_DEPTH,
|
|
151
|
+
bufferCount: 4096,
|
|
152
|
+
bufferSize: 2048,
|
|
153
|
+
...options
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const info = server.startBatch((events) => {
|
|
157
|
+
for (const event of events) {
|
|
158
|
+
if (event.eventType === 'data') {
|
|
159
|
+
server.sendAndClose(event.connectionId, RESPONSE);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
return {
|
|
166
|
+
...(await runLoad(info.port)),
|
|
167
|
+
serverInfo: summarizeServerInfo(server.info())
|
|
168
|
+
};
|
|
169
|
+
} finally {
|
|
170
|
+
server.stop();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function withUringFullBatchServer(options = {}) {
|
|
175
|
+
const server = new UringTcpServer({
|
|
176
|
+
host: '127.0.0.1',
|
|
177
|
+
port: 0,
|
|
178
|
+
queueDepth: QUEUE_DEPTH,
|
|
179
|
+
bufferCount: 4096,
|
|
180
|
+
bufferSize: 2048,
|
|
181
|
+
...options
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const info = server.startBatch((events) => {
|
|
185
|
+
const sends = [];
|
|
186
|
+
for (const event of events) {
|
|
187
|
+
if (event.eventType === 'data') {
|
|
188
|
+
sends.push({ connectionId: event.connectionId, data: RESPONSE });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (sends.length > 0) {
|
|
192
|
+
server.sendBatchAndClose(sends);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
return {
|
|
198
|
+
...(await runLoad(info.port)),
|
|
199
|
+
serverInfo: summarizeServerInfo(server.info())
|
|
200
|
+
};
|
|
201
|
+
} finally {
|
|
202
|
+
server.stop();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function withTcpFacadeServer(options = {}) {
|
|
207
|
+
const server = createTcpServer(
|
|
208
|
+
{
|
|
209
|
+
host: '127.0.0.1',
|
|
210
|
+
port: 0,
|
|
211
|
+
queueDepth: QUEUE_DEPTH,
|
|
212
|
+
bufferCount: 4096,
|
|
213
|
+
bufferSize: 2048,
|
|
214
|
+
...options
|
|
215
|
+
},
|
|
216
|
+
(connection) => {
|
|
217
|
+
connection.once('data', () => {
|
|
218
|
+
connection.end(RESPONSE);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
server.listen();
|
|
224
|
+
const info = server.info();
|
|
225
|
+
try {
|
|
226
|
+
return {
|
|
227
|
+
...(await runLoad(info.port)),
|
|
228
|
+
serverInfo: summarizeServerInfo(server.info())
|
|
229
|
+
};
|
|
230
|
+
} finally {
|
|
231
|
+
server.close();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function withTcpFacadeBatchServer(options = {}) {
|
|
236
|
+
const server = createTcpServer({
|
|
237
|
+
host: '127.0.0.1',
|
|
238
|
+
port: 0,
|
|
239
|
+
queueDepth: QUEUE_DEPTH,
|
|
240
|
+
bufferCount: 4096,
|
|
241
|
+
bufferSize: 2048,
|
|
242
|
+
...options
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
server.on('data', (connection) => {
|
|
246
|
+
server.sendBatchAndClose([{ connection, data: RESPONSE }]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
server.listen();
|
|
250
|
+
const info = server.info();
|
|
251
|
+
try {
|
|
252
|
+
return {
|
|
253
|
+
...(await runLoad(info.port)),
|
|
254
|
+
serverInfo: summarizeServerInfo(server.info())
|
|
255
|
+
};
|
|
256
|
+
} finally {
|
|
257
|
+
server.close();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function percentile(values, quantile) {
|
|
262
|
+
if (values.length === 0) return 0;
|
|
263
|
+
const index = Math.min(values.length - 1, Math.floor(values.length * quantile));
|
|
264
|
+
return Number(values[index].toFixed(3));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function maybeRecvBundle(label, runner) {
|
|
268
|
+
if (!CAPS.recvBundle) {
|
|
269
|
+
const result = {
|
|
270
|
+
skipped: 'kernel does not report IORING_FEAT_RECVSEND_BUNDLE'
|
|
271
|
+
};
|
|
272
|
+
console.log(label, result);
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
const result = await runner();
|
|
276
|
+
console.log(label, result);
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function summarizeServerInfo(info) {
|
|
281
|
+
if (!info) return null;
|
|
282
|
+
return {
|
|
283
|
+
backend: info.backend,
|
|
284
|
+
backlog: info.backlog,
|
|
285
|
+
queueDepth: info.queueDepth,
|
|
286
|
+
bufferCount: info.bufferCount,
|
|
287
|
+
bufferSize: info.bufferSize,
|
|
288
|
+
tcpNoDelay: info.tcpNoDelay,
|
|
289
|
+
reusePort: info.reusePort,
|
|
290
|
+
tcpDeferAcceptSeconds: info.tcpDeferAcceptSeconds,
|
|
291
|
+
socketRecvBufferSize: info.socketRecvBufferSize,
|
|
292
|
+
socketSendBufferSize: info.socketSendBufferSize,
|
|
293
|
+
multishotAccept: info.multishotAccept,
|
|
294
|
+
multishotRecv: info.multishotRecv,
|
|
295
|
+
providedBufferRing: info.providedBufferRing,
|
|
296
|
+
recvBundle: info.recvBundle,
|
|
297
|
+
recvCopyEvents: info.recvCopyEvents,
|
|
298
|
+
recvCopyBytes: info.recvCopyBytes,
|
|
299
|
+
eventBatchSize: info.eventBatchSize,
|
|
300
|
+
sendBufferCount: info.sendBufferCount,
|
|
301
|
+
sendBufferSize: info.sendBufferSize,
|
|
302
|
+
registeredSendBuffer: info.registeredSendBuffer,
|
|
303
|
+
fixedSendBufferMisses: info.fixedSendBufferMisses,
|
|
304
|
+
fixedSendBufferMissBytes: info.fixedSendBufferMissBytes,
|
|
305
|
+
zeroCopySend: info.zeroCopySend,
|
|
306
|
+
zeroCopyReceive: info.zeroCopyReceive,
|
|
307
|
+
zcrxReady: info.zcrxReady
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function baseReport() {
|
|
312
|
+
return {
|
|
313
|
+
mode: 'tcp-echo-matrix',
|
|
314
|
+
status: 'running',
|
|
315
|
+
startedAt: new Date().toISOString(),
|
|
316
|
+
finishedAt: null,
|
|
317
|
+
config: {
|
|
318
|
+
durationMs: DURATION_MS,
|
|
319
|
+
concurrency: CONCURRENCY,
|
|
320
|
+
queueDepth: QUEUE_DEPTH,
|
|
321
|
+
bundleRequestSize: BUNDLE_REQUEST_SIZE
|
|
322
|
+
},
|
|
323
|
+
capabilities: CAPS,
|
|
324
|
+
results: [],
|
|
325
|
+
error: null
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function writeReport(report) {
|
|
330
|
+
if (!REPORT_PATH) return;
|
|
331
|
+
fs.mkdirSync(path.dirname(REPORT_PATH), { recursive: true });
|
|
332
|
+
fs.writeFileSync(REPORT_PATH, `${JSON.stringify(report, null, 2)}\n`);
|
|
333
|
+
console.log(`TCP benchmark report written: ${REPORT_PATH}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function errorForReport(error) {
|
|
337
|
+
return {
|
|
338
|
+
name: error && error.name ? error.name : 'Error',
|
|
339
|
+
message: error && error.message ? error.message : String(error),
|
|
340
|
+
stack: error && error.stack ? error.stack : undefined
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function record(report, caseName, runner) {
|
|
345
|
+
const result = await runner();
|
|
346
|
+
report.results.push({ caseName, result });
|
|
347
|
+
console.log(caseName, result);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
(async () => {
|
|
351
|
+
const report = baseReport();
|
|
352
|
+
try {
|
|
353
|
+
console.log(report.config);
|
|
354
|
+
await record(report, 'node:net echo', withNodeServer);
|
|
355
|
+
await record(report, 'ferrings native tcp echo', withUringNativeEchoServer);
|
|
356
|
+
report.results.push({
|
|
357
|
+
caseName: 'ferrings native tcp echo recv-bundle',
|
|
358
|
+
result: await maybeRecvBundle('ferrings native tcp echo recv-bundle', () =>
|
|
359
|
+
withUringNativeEchoServer(
|
|
360
|
+
{
|
|
361
|
+
bufferSize: 512,
|
|
362
|
+
useRecvBundle: true
|
|
363
|
+
},
|
|
364
|
+
BUNDLE_REQUEST
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
});
|
|
368
|
+
await record(report, 'ferrings tcp echo', withUringServer);
|
|
369
|
+
await record(report, 'ferrings tcp echo batch', withUringBatchServer);
|
|
370
|
+
await record(report, 'ferrings tcp echo full batch', withUringFullBatchServer);
|
|
371
|
+
await record(report, 'ferrings tcp facade echo', withTcpFacadeServer);
|
|
372
|
+
await record(report, 'ferrings tcp facade batch echo', withTcpFacadeBatchServer);
|
|
373
|
+
await record(report, 'ferrings tcp echo zc', () =>
|
|
374
|
+
withUringServer({
|
|
375
|
+
useZeroCopySend: true,
|
|
376
|
+
sendBufferCount: 512,
|
|
377
|
+
sendBufferSize: 2048
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
await record(report, 'ferrings native tcp echo zc', () =>
|
|
381
|
+
withUringNativeEchoServer({
|
|
382
|
+
useZeroCopySend: true,
|
|
383
|
+
sendBufferCount: 512,
|
|
384
|
+
sendBufferSize: 2048
|
|
385
|
+
})
|
|
386
|
+
);
|
|
387
|
+
report.results.push({
|
|
388
|
+
caseName: 'ferrings native tcp echo zc recv-bundle',
|
|
389
|
+
result: await maybeRecvBundle('ferrings native tcp echo zc recv-bundle', () =>
|
|
390
|
+
withUringNativeEchoServer(
|
|
391
|
+
{
|
|
392
|
+
bufferSize: 512,
|
|
393
|
+
useRecvBundle: true,
|
|
394
|
+
useZeroCopySend: true,
|
|
395
|
+
sendBufferCount: 512,
|
|
396
|
+
sendBufferSize: Math.max(8192, BUNDLE_REQUEST_SIZE * 2)
|
|
397
|
+
},
|
|
398
|
+
BUNDLE_REQUEST
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
});
|
|
402
|
+
await record(report, 'ferrings tcp echo batch zc', () =>
|
|
403
|
+
withUringBatchServer({
|
|
404
|
+
useZeroCopySend: true,
|
|
405
|
+
sendBufferCount: 512,
|
|
406
|
+
sendBufferSize: 2048
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
await record(report, 'ferrings tcp echo full batch zc', () =>
|
|
410
|
+
withUringFullBatchServer({
|
|
411
|
+
useZeroCopySend: true,
|
|
412
|
+
sendBufferCount: 512,
|
|
413
|
+
sendBufferSize: 2048
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
await record(report, 'ferrings tcp facade echo zc', () =>
|
|
417
|
+
withTcpFacadeServer({
|
|
418
|
+
useZeroCopySend: true,
|
|
419
|
+
sendBufferCount: 512,
|
|
420
|
+
sendBufferSize: 2048
|
|
421
|
+
})
|
|
422
|
+
);
|
|
423
|
+
await record(report, 'ferrings tcp facade batch echo zc', () =>
|
|
424
|
+
withTcpFacadeBatchServer({
|
|
425
|
+
useZeroCopySend: true,
|
|
426
|
+
sendBufferCount: 512,
|
|
427
|
+
sendBufferSize: 2048
|
|
428
|
+
})
|
|
429
|
+
);
|
|
430
|
+
report.status = 'passed';
|
|
431
|
+
} catch (error) {
|
|
432
|
+
report.status = 'failed';
|
|
433
|
+
report.error = errorForReport(error);
|
|
434
|
+
throw error;
|
|
435
|
+
} finally {
|
|
436
|
+
report.finishedAt = new Date().toISOString();
|
|
437
|
+
writeReport(report);
|
|
438
|
+
}
|
|
439
|
+
})().catch((error) => {
|
|
440
|
+
console.error(error);
|
|
441
|
+
process.exitCode = 1;
|
|
442
|
+
});
|