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.
@@ -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
+ });