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
package/bin/ferrings.js
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const { capabilities, zcrxProbe } = require('..');
|
|
6
|
+
const { runQueueStatsParserSelfTest, runZcrxHardwareSmoke } = require('../zcrx-smoke');
|
|
7
|
+
const pkg = require('../package.json');
|
|
8
|
+
|
|
9
|
+
class CliError extends Error {
|
|
10
|
+
constructor(message, exitCode) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.exitCode = exitCode;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const command = args[0] && !args[0].startsWith('-') ? args.shift() : 'capabilities';
|
|
18
|
+
|
|
19
|
+
main().catch((error) => {
|
|
20
|
+
handleError(error);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
25
|
+
printHelp();
|
|
26
|
+
} else if (command === 'capabilities' || command === 'caps') {
|
|
27
|
+
runCapabilities(args);
|
|
28
|
+
} else if (command === 'doctor') {
|
|
29
|
+
runDoctor(args);
|
|
30
|
+
} else if (command === 'zcrx-probe' || command === 'zcrx') {
|
|
31
|
+
runZcrxProbe(args);
|
|
32
|
+
} else if (command === 'zcrx-smoke') {
|
|
33
|
+
await runZcrxSmoke(args);
|
|
34
|
+
} else {
|
|
35
|
+
throw new CliError(`unknown command: ${command}`, 64);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleError(error) {
|
|
40
|
+
console.error(error.message || String(error));
|
|
41
|
+
if (!(error instanceof CliError)) {
|
|
42
|
+
console.error(error.stack);
|
|
43
|
+
}
|
|
44
|
+
process.exitCode = error.exitCode || 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function runCapabilities(rawArgs) {
|
|
48
|
+
const options = parseFlags(rawArgs, {
|
|
49
|
+
booleans: ['json', 'compact', 'help']
|
|
50
|
+
});
|
|
51
|
+
if (options.help) {
|
|
52
|
+
printHelp();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const report = baseReport('capabilities');
|
|
56
|
+
report.capabilities = capabilities();
|
|
57
|
+
if (options.json || options.compact) {
|
|
58
|
+
printJson(report, options.compact);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
printCapabilities(report.capabilities);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runDoctor(rawArgs) {
|
|
65
|
+
const options = parseFlags(rawArgs, {
|
|
66
|
+
booleans: ['active', 'compact', 'help', 'json', 'require-ready'],
|
|
67
|
+
values: ['interface', 'rx-queue', 'rx-buffer-size']
|
|
68
|
+
});
|
|
69
|
+
if (options.help) {
|
|
70
|
+
printHelp();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const report = buildDoctorReport(options);
|
|
75
|
+
if (options.json || options.compact) {
|
|
76
|
+
printJson(report, options.compact);
|
|
77
|
+
} else {
|
|
78
|
+
printDoctorReport(report);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (options['require-ready'] && !report.ready) {
|
|
82
|
+
throw new CliError('doctor readiness requirements were not met', 2);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function runZcrxProbe(rawArgs) {
|
|
87
|
+
const options = parseFlags(rawArgs, {
|
|
88
|
+
booleans: ['active', 'all', 'compact', 'help', 'json', 'require-ready'],
|
|
89
|
+
values: ['interface', 'rx-queue', 'rx-buffer-size']
|
|
90
|
+
});
|
|
91
|
+
if (options.help) {
|
|
92
|
+
printHelp();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const report = baseReport('zcrx-probe');
|
|
97
|
+
report.capabilities = capabilities();
|
|
98
|
+
const probeOptions = {
|
|
99
|
+
rxQueue: numberOption(options['rx-queue'], 'rx-queue'),
|
|
100
|
+
rxBufferSize: numberOption(options['rx-buffer-size'], 'rx-buffer-size'),
|
|
101
|
+
activeRegistration: Boolean(options.active)
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (options.all) {
|
|
105
|
+
report.probes = listInterfaces().map((interfaceName) =>
|
|
106
|
+
zcrxProbe({
|
|
107
|
+
...probeOptions,
|
|
108
|
+
interfaceName
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
} else {
|
|
112
|
+
report.probe = zcrxProbe({
|
|
113
|
+
...probeOptions,
|
|
114
|
+
interfaceName: options.interface
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const probes = report.probes || [report.probe];
|
|
119
|
+
report.ready = probes.every((probe) => probe && probe.ready);
|
|
120
|
+
if (options.json || options.compact) {
|
|
121
|
+
printJson(report, options.compact);
|
|
122
|
+
} else {
|
|
123
|
+
printZcrxReport(report);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (options['require-ready'] && !report.ready) {
|
|
127
|
+
throw new CliError('ZCRX readiness requirements were not met', 2);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function runZcrxSmoke(rawArgs) {
|
|
132
|
+
const options = parseFlags(rawArgs, {
|
|
133
|
+
booleans: [
|
|
134
|
+
'compact',
|
|
135
|
+
'help',
|
|
136
|
+
'json',
|
|
137
|
+
'require-rx-queue-stats',
|
|
138
|
+
'self-test'
|
|
139
|
+
],
|
|
140
|
+
values: [
|
|
141
|
+
'bind-host',
|
|
142
|
+
'connect-host',
|
|
143
|
+
'interface',
|
|
144
|
+
'report-path',
|
|
145
|
+
'rx-buffer-size',
|
|
146
|
+
'rx-queue',
|
|
147
|
+
'timeout-ms'
|
|
148
|
+
]
|
|
149
|
+
});
|
|
150
|
+
if (options.help) {
|
|
151
|
+
printHelp();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (options['self-test']) {
|
|
155
|
+
runQueueStatsParserSelfTest();
|
|
156
|
+
console.log('zcrx smoke self-test ok');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const report = await runZcrxHardwareSmoke({
|
|
162
|
+
interfaceName: options.interface,
|
|
163
|
+
rxQueue: numberOption(options['rx-queue'], 'rx-queue'),
|
|
164
|
+
rxBufferSize: numberOption(options['rx-buffer-size'], 'rx-buffer-size'),
|
|
165
|
+
bindHost: options['bind-host'],
|
|
166
|
+
connectHost: options['connect-host'],
|
|
167
|
+
timeoutMs: numberOption(options['timeout-ms'], 'timeout-ms'),
|
|
168
|
+
requireRxQueueStats: Boolean(options['require-rx-queue-stats']),
|
|
169
|
+
reportPath: options['report-path']
|
|
170
|
+
});
|
|
171
|
+
if (options.json || options.compact) {
|
|
172
|
+
printJson(report, options.compact);
|
|
173
|
+
} else {
|
|
174
|
+
printZcrxSmokeReport(report);
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (error.report && (options.json || options.compact)) {
|
|
178
|
+
printJson(error.report, options.compact);
|
|
179
|
+
}
|
|
180
|
+
error.exitCode = 1;
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function baseReport(mode) {
|
|
186
|
+
return {
|
|
187
|
+
package: pkg.name,
|
|
188
|
+
version: pkg.version,
|
|
189
|
+
mode,
|
|
190
|
+
generatedAt: new Date().toISOString()
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildDoctorReport(options) {
|
|
195
|
+
const report = baseReport('doctor');
|
|
196
|
+
const probeOptions = {
|
|
197
|
+
interfaceName: options.interface,
|
|
198
|
+
rxQueue: numberOption(options['rx-queue'], 'rx-queue'),
|
|
199
|
+
rxBufferSize: numberOption(options['rx-buffer-size'], 'rx-buffer-size'),
|
|
200
|
+
activeRegistration: Boolean(options.active)
|
|
201
|
+
};
|
|
202
|
+
report.capabilities = capabilities();
|
|
203
|
+
report.transport = buildTransportVerdict(report.capabilities);
|
|
204
|
+
report.zcrx = zcrxProbe(probeOptions);
|
|
205
|
+
report.ready = report.transport.ready && report.zcrx.ready;
|
|
206
|
+
report.verdict = doctorVerdict(report);
|
|
207
|
+
report.blockers = [...report.transport.blockers, ...report.zcrx.blockers];
|
|
208
|
+
report.warnings = [...report.transport.warnings];
|
|
209
|
+
report.nextCommand = doctorNextCommand(report, probeOptions);
|
|
210
|
+
return report;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function buildTransportVerdict(caps) {
|
|
214
|
+
const blockers = [];
|
|
215
|
+
const warnings = [];
|
|
216
|
+
if (caps.platform !== 'linux') {
|
|
217
|
+
blockers.push('ferrings transport requires Linux');
|
|
218
|
+
}
|
|
219
|
+
if (!caps.ioUringAvailable) {
|
|
220
|
+
blockers.push('io_uring is not available');
|
|
221
|
+
}
|
|
222
|
+
if (!caps.acceptMulti) {
|
|
223
|
+
blockers.push('multishot accept is not available');
|
|
224
|
+
}
|
|
225
|
+
if (!caps.recvMulti) {
|
|
226
|
+
blockers.push('multishot recv is not available');
|
|
227
|
+
}
|
|
228
|
+
if (!caps.send) {
|
|
229
|
+
blockers.push('io_uring send is not available');
|
|
230
|
+
}
|
|
231
|
+
if (!caps.providedBufferRing) {
|
|
232
|
+
warnings.push('provided-buffer ring registration failed; servers will use the legacy provided-buffer fallback');
|
|
233
|
+
}
|
|
234
|
+
if (!caps.sendZc) {
|
|
235
|
+
warnings.push('IORING_OP_SEND_ZC is unavailable; zero-copy send will fall back or fail when required');
|
|
236
|
+
}
|
|
237
|
+
if (!caps.registeredSendBuffer) {
|
|
238
|
+
warnings.push(`registered fixed-buffer send probe failed: ${caps.registeredSendBufferProbe}`);
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
ready: blockers.length === 0,
|
|
242
|
+
blockers,
|
|
243
|
+
warnings
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function doctorVerdict(report) {
|
|
248
|
+
if (report.ready) return 'ready';
|
|
249
|
+
if (!report.transport.ready) return 'transport-blocked';
|
|
250
|
+
return 'transport-ready-zcrx-blocked';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function doctorNextCommand(report, probeOptions) {
|
|
254
|
+
if (!report.transport.ready) {
|
|
255
|
+
return 'ferrings capabilities --json';
|
|
256
|
+
}
|
|
257
|
+
const interfaceName = report.zcrx.interfaceName || probeOptions.interfaceName;
|
|
258
|
+
const rxQueue = report.zcrx.rxQueue;
|
|
259
|
+
const rxBufferSize = report.zcrx.rxBufferSize;
|
|
260
|
+
if (!report.zcrx.ready) {
|
|
261
|
+
if (!interfaceName) {
|
|
262
|
+
return 'ferrings doctor --interface <nic> --active --json';
|
|
263
|
+
}
|
|
264
|
+
return commandLine([
|
|
265
|
+
'ferrings',
|
|
266
|
+
'zcrx-probe',
|
|
267
|
+
'--interface',
|
|
268
|
+
interfaceName,
|
|
269
|
+
'--rx-queue',
|
|
270
|
+
String(rxQueue),
|
|
271
|
+
...(rxBufferSize ? ['--rx-buffer-size', String(rxBufferSize)] : []),
|
|
272
|
+
'--active',
|
|
273
|
+
'--json'
|
|
274
|
+
]);
|
|
275
|
+
}
|
|
276
|
+
return commandLine([
|
|
277
|
+
'ferrings',
|
|
278
|
+
'zcrx-smoke',
|
|
279
|
+
'--interface',
|
|
280
|
+
interfaceName || '<nic>',
|
|
281
|
+
'--rx-queue',
|
|
282
|
+
String(rxQueue),
|
|
283
|
+
...(rxBufferSize ? ['--rx-buffer-size', String(rxBufferSize)] : []),
|
|
284
|
+
'--connect-host',
|
|
285
|
+
'<host-routed-to-nic>',
|
|
286
|
+
'--json'
|
|
287
|
+
]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function commandLine(parts) {
|
|
291
|
+
return parts.map(quoteCommandArg).join(' ');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function quoteCommandArg(value) {
|
|
295
|
+
if (/^[A-Za-z0-9_./:=@+-]+$/.test(value) || /^<[^>\s]+>$/.test(value)) {
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function parseFlags(rawArgs, schema) {
|
|
302
|
+
const options = {};
|
|
303
|
+
const booleans = new Set(schema.booleans || []);
|
|
304
|
+
const values = new Set(schema.values || []);
|
|
305
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
306
|
+
const arg = rawArgs[index];
|
|
307
|
+
if (!arg.startsWith('-')) {
|
|
308
|
+
throw new CliError(`unexpected argument: ${arg}`, 64);
|
|
309
|
+
}
|
|
310
|
+
const flag = parseFlagToken(arg);
|
|
311
|
+
const rawName = flag.rawName;
|
|
312
|
+
const inlineValue = flag.inlineValue;
|
|
313
|
+
const name = normalizeFlagName(rawName);
|
|
314
|
+
if (booleans.has(name)) {
|
|
315
|
+
options[name] = inlineValue === undefined ? true : inlineValue !== 'false';
|
|
316
|
+
} else if (values.has(name)) {
|
|
317
|
+
const value = inlineValue === undefined ? rawArgs[++index] : inlineValue;
|
|
318
|
+
if (value === undefined || value.startsWith('-')) {
|
|
319
|
+
throw new CliError(`${flag.displayName} requires a value`, 64);
|
|
320
|
+
}
|
|
321
|
+
options[name] = value;
|
|
322
|
+
} else {
|
|
323
|
+
throw new CliError(`unknown option: ${flag.displayName}`, 64);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return options;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function parseFlagToken(arg) {
|
|
330
|
+
const isLong = arg.startsWith('--');
|
|
331
|
+
const prefix = isLong ? '--' : '-';
|
|
332
|
+
const body = arg.slice(prefix.length);
|
|
333
|
+
if (body.length === 0) {
|
|
334
|
+
throw new CliError(`unknown option: ${arg}`, 64);
|
|
335
|
+
}
|
|
336
|
+
const [rawName, inlineValue] = body.split(/=(.*)/s, 2);
|
|
337
|
+
return {
|
|
338
|
+
rawName,
|
|
339
|
+
inlineValue,
|
|
340
|
+
displayName: `${prefix}${rawName}`
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function normalizeFlagName(name) {
|
|
345
|
+
if (name === 'interface-name') return 'interface';
|
|
346
|
+
if (name === 'i') return 'interface';
|
|
347
|
+
if (name === 'h') return 'help';
|
|
348
|
+
return name;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function numberOption(value, name) {
|
|
352
|
+
if (value === undefined) return undefined;
|
|
353
|
+
const number = Number(value);
|
|
354
|
+
if (!Number.isInteger(number) || number < 0) {
|
|
355
|
+
throw new CliError(`--${name} must be a non-negative integer`, 64);
|
|
356
|
+
}
|
|
357
|
+
return number;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function listInterfaces() {
|
|
361
|
+
const root = '/sys/class/net';
|
|
362
|
+
try {
|
|
363
|
+
return fs
|
|
364
|
+
.readdirSync(root)
|
|
365
|
+
.filter((name) => !name.startsWith('.'))
|
|
366
|
+
.sort();
|
|
367
|
+
} catch {
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function printCapabilities(caps) {
|
|
373
|
+
console.log(`ferrings ${pkg.version}`);
|
|
374
|
+
console.log(`platform: ${caps.platform}`);
|
|
375
|
+
console.log(`kernel: ${caps.kernelRelease}`);
|
|
376
|
+
console.log(`io_uring: ${yesNo(caps.ioUringAvailable)}`);
|
|
377
|
+
console.log(`multishot accept: ${yesNo(caps.acceptMulti)}`);
|
|
378
|
+
console.log(`multishot recv: ${yesNo(caps.recvMulti)}`);
|
|
379
|
+
console.log(`provided buffer ring: ${yesNo(caps.providedBufferRing)} (${caps.providedBufferRingProbe})`);
|
|
380
|
+
console.log(`recv bundle: ${yesNo(caps.recvBundle)}`);
|
|
381
|
+
console.log(`send zc: ${yesNo(caps.sendZc)}`);
|
|
382
|
+
console.log(`registered send buffer: ${yesNo(caps.registeredSendBuffer)} (${caps.registeredSendBufferProbe})`);
|
|
383
|
+
console.log(`recv zc opcode: ${yesNo(caps.recvZc)}`);
|
|
384
|
+
console.log(`ZCRX CQE32 ring: ${yesNo(caps.zcrxCqe32Ring)} (${caps.zcrxCqe32RingProbe})`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function printZcrxReport(report) {
|
|
388
|
+
printCapabilities(report.capabilities);
|
|
389
|
+
const probes = report.probes || [report.probe];
|
|
390
|
+
console.log('');
|
|
391
|
+
for (const probe of probes) {
|
|
392
|
+
console.log(`ZCRX interface: ${probe.interfaceName || '(none)'}`);
|
|
393
|
+
console.log(` ready: ${yesNo(probe.ready)}`);
|
|
394
|
+
console.log(` ifindex: ${probe.interfaceIndex}`);
|
|
395
|
+
console.log(` operstate: ${probe.operstate || 'unknown'}`);
|
|
396
|
+
console.log(` driver: ${probe.driver || 'unknown'}`);
|
|
397
|
+
console.log(` rx queue: ${probe.rxQueue}/${probe.rxQueueCount}`);
|
|
398
|
+
console.log(` rx buffer size: ${probe.rxBufferSize}`);
|
|
399
|
+
console.log(` header/data split: ${probe.headerDataSplit}`);
|
|
400
|
+
console.log(` flow steering: ${probe.flowSteering}`);
|
|
401
|
+
if (probe.activeRegistration) {
|
|
402
|
+
console.log(` active registration: ${probe.activeRegistrationResult || 'unknown'}`);
|
|
403
|
+
}
|
|
404
|
+
if (probe.blockers.length > 0) {
|
|
405
|
+
console.log(' blockers:');
|
|
406
|
+
for (const blocker of probe.blockers) {
|
|
407
|
+
console.log(` - ${blocker}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function printDoctorReport(report) {
|
|
414
|
+
printCapabilities(report.capabilities);
|
|
415
|
+
console.log('');
|
|
416
|
+
console.log(`doctor verdict: ${report.verdict}`);
|
|
417
|
+
console.log(`transport core: ${report.transport.ready ? 'ready' : 'blocked'}`);
|
|
418
|
+
for (const blocker of report.transport.blockers) {
|
|
419
|
+
console.log(` blocker: ${blocker}`);
|
|
420
|
+
}
|
|
421
|
+
for (const warning of report.transport.warnings) {
|
|
422
|
+
console.log(` warning: ${warning}`);
|
|
423
|
+
}
|
|
424
|
+
console.log(`ZCRX: ${report.zcrx.ready ? 'ready' : 'blocked'}`);
|
|
425
|
+
console.log(` interface: ${report.zcrx.interfaceName || '(none)'}`);
|
|
426
|
+
console.log(` rx queue: ${report.zcrx.rxQueue}/${report.zcrx.rxQueueCount}`);
|
|
427
|
+
console.log(` rx buffer size: ${report.zcrx.rxBufferSize}`);
|
|
428
|
+
console.log(` active registration: ${report.zcrx.activeRegistration ? report.zcrx.activeRegistrationResult || 'unknown' : 'not requested'}`);
|
|
429
|
+
for (const blocker of report.zcrx.blockers) {
|
|
430
|
+
console.log(` blocker: ${blocker}`);
|
|
431
|
+
}
|
|
432
|
+
console.log(`next: ${report.nextCommand}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function printZcrxSmokeReport(report) {
|
|
436
|
+
if (report.status === 'skipped') {
|
|
437
|
+
console.log(`ZCRX smoke skipped: ${report.skippedReason}`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (report.status === 'self-test') {
|
|
441
|
+
console.log('ZCRX smoke self-test ok');
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
console.log(`ZCRX smoke status: ${report.status}`);
|
|
445
|
+
console.log(`interface: ${report.config.interfaceName}`);
|
|
446
|
+
console.log(`rx queue: ${report.config.rxQueue}`);
|
|
447
|
+
console.log(`bind host: ${report.config.bindHost}`);
|
|
448
|
+
console.log(`connect host: ${report.config.connectHost}`);
|
|
449
|
+
if (report.probe) {
|
|
450
|
+
console.log(`active registration: ${report.probe.activeRegistrationResult || 'unknown'}`);
|
|
451
|
+
}
|
|
452
|
+
for (const warning of report.warnings || []) {
|
|
453
|
+
console.log(`warning: ${warning}`);
|
|
454
|
+
}
|
|
455
|
+
for (const smoke of report.smokes || []) {
|
|
456
|
+
console.log(`${smoke.name}: ${smoke.status}`);
|
|
457
|
+
}
|
|
458
|
+
if (report.queueCounters && report.queueCounters.positiveDeltas.length > 0) {
|
|
459
|
+
console.log(
|
|
460
|
+
`rx queue counter evidence: ${report.queueCounters.positiveDeltas
|
|
461
|
+
.map(({ name, delta }) => `${name}+${delta}`)
|
|
462
|
+
.join(', ')}`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function printJson(value, compact) {
|
|
468
|
+
console.log(JSON.stringify(value, null, compact ? 0 : 2));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function yesNo(value) {
|
|
472
|
+
return value ? 'yes' : 'no';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function printHelp() {
|
|
476
|
+
console.log(`Usage:
|
|
477
|
+
ferrings capabilities [--json|--compact]
|
|
478
|
+
ferrings doctor [--interface <name>] [--rx-queue <n>] [--rx-buffer-size <n>] [--active] [--require-ready] [--json|--compact]
|
|
479
|
+
ferrings zcrx-probe [--interface <name>] [--rx-queue <n>] [--rx-buffer-size <n>] [--active] [--all] [--require-ready] [--json|--compact]
|
|
480
|
+
ferrings zcrx-smoke [--interface <name>] [--connect-host <host>] [--bind-host <host>] [--rx-queue <n>] [--rx-buffer-size <n>] [--timeout-ms <n>] [--require-rx-queue-stats] [--report-path <path>] [--json|--compact]
|
|
481
|
+
|
|
482
|
+
Commands:
|
|
483
|
+
capabilities Print kernel/io_uring feature probes.
|
|
484
|
+
doctor Print one installed-package transport/ZCRX readiness verdict.
|
|
485
|
+
zcrx-probe Print ZCRX NIC readiness probes.
|
|
486
|
+
zcrx-smoke Run HTTP/native echo/programmable TCP ZCRX traffic proof.
|
|
487
|
+
|
|
488
|
+
Options:
|
|
489
|
+
--json Print a pretty JSON report.
|
|
490
|
+
--compact Print compact JSON.
|
|
491
|
+
--interface, -i Probe a specific interface.
|
|
492
|
+
--rx-queue Probe a specific RX queue.
|
|
493
|
+
--rx-buffer-size Try a specific ZCRX receive buffer size; 0 uses the kernel default.
|
|
494
|
+
--active Attempt short-lived active ZCRX IFQ registration.
|
|
495
|
+
--all Probe every interface under /sys/class/net.
|
|
496
|
+
--require-ready Exit 2 when selected ZCRX probes are not ready.
|
|
497
|
+
--connect-host Host used by client traffic for zcrx-smoke.
|
|
498
|
+
--bind-host Host used by zcrx-smoke servers; defaults to 0.0.0.0.
|
|
499
|
+
--timeout-ms Per-request zcrx-smoke timeout.
|
|
500
|
+
--report-path Write the zcrx-smoke JSON report to a file.
|
|
501
|
+
--self-test Run the zcrx-smoke parser self-test.
|
|
502
|
+
`);
|
|
503
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { UringHttpServer } = require('..');
|
|
4
|
+
|
|
5
|
+
const server = new UringHttpServer({
|
|
6
|
+
host: process.env.HOST || '127.0.0.1',
|
|
7
|
+
port: Number(process.env.PORT || 0),
|
|
8
|
+
backlog: Number(process.env.BACKLOG || 1024),
|
|
9
|
+
queueDepth: Number(process.env.QUEUE_DEPTH || 1024),
|
|
10
|
+
bufferCount: Number(process.env.BUFFER_COUNT || 4096),
|
|
11
|
+
bufferSize: Number(process.env.BUFFER_SIZE || 2048),
|
|
12
|
+
responseBody: process.env.RESPONSE_BODY || 'hello from ferrings\n',
|
|
13
|
+
useRegisteredSendBuffer: process.env.USE_REGISTERED_SEND_BUFFER === '1',
|
|
14
|
+
useZeroCopySend: process.env.USE_ZERO_COPY_SEND === '1'
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const info = server.start();
|
|
18
|
+
const keepAlive = setInterval(() => {}, 1 << 30);
|
|
19
|
+
console.log(JSON.stringify({ listening: `http://${info.host}:${info.port}`, info }, null, 2));
|
|
20
|
+
|
|
21
|
+
process.on('SIGINT', () => {
|
|
22
|
+
server.stop();
|
|
23
|
+
clearInterval(keepAlive);
|
|
24
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createTcpServer } = require('..');
|
|
4
|
+
|
|
5
|
+
const server = createTcpServer((connection) => {
|
|
6
|
+
connection.on('data', (data) => {
|
|
7
|
+
connection.end(data);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
server.listen(
|
|
12
|
+
{
|
|
13
|
+
host: process.env.HOST || '127.0.0.1',
|
|
14
|
+
port: Number(process.env.PORT || 0),
|
|
15
|
+
backlog: Number(process.env.BACKLOG || 1024),
|
|
16
|
+
queueDepth: Number(process.env.QUEUE_DEPTH || 1024),
|
|
17
|
+
bufferCount: Number(process.env.BUFFER_COUNT || 4096),
|
|
18
|
+
bufferSize: Number(process.env.BUFFER_SIZE || 2048),
|
|
19
|
+
useRecvBundle: process.env.USE_RECV_BUNDLE === '1',
|
|
20
|
+
useZeroCopySend: process.env.USE_ZERO_COPY_SEND === '1'
|
|
21
|
+
},
|
|
22
|
+
(info) => {
|
|
23
|
+
console.log(JSON.stringify({ listening: `tcp://${info.host}:${info.port}`, info }, null, 2));
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
process.on('SIGINT', () => {
|
|
28
|
+
server.close();
|
|
29
|
+
});
|
|
Binary file
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import type { ServerInfo, TcpServerOptions } from './native'
|
|
3
|
+
|
|
4
|
+
export * from './native'
|
|
5
|
+
|
|
6
|
+
export declare class IoUringTcpConnection extends EventEmitter {
|
|
7
|
+
readonly id: number
|
|
8
|
+
readonly remoteAddress?: string
|
|
9
|
+
readonly remoteFamily?: 'IPv4' | 'IPv6'
|
|
10
|
+
readonly remotePort?: number
|
|
11
|
+
readonly destroyed: boolean
|
|
12
|
+
write(data: Buffer | string | Uint8Array): boolean
|
|
13
|
+
end(data?: Buffer | string | Uint8Array): boolean
|
|
14
|
+
destroy(): boolean
|
|
15
|
+
on(event: 'data', listener: (data: Buffer) => void): this
|
|
16
|
+
on(event: 'close', listener: () => void): this
|
|
17
|
+
on(event: string | symbol, listener: (...args: Array<any>) => void): this
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export declare class IoUringTcpTransportServer extends EventEmitter {
|
|
21
|
+
constructor(
|
|
22
|
+
options?: TcpServerOptions | undefined | null,
|
|
23
|
+
connectionListener?: (connection: IoUringTcpConnection) => unknown
|
|
24
|
+
)
|
|
25
|
+
constructor(connectionListener?: (connection: IoUringTcpConnection) => unknown)
|
|
26
|
+
start(callback?: (info: ServerInfo) => unknown): ServerInfo
|
|
27
|
+
start(options?: TcpServerOptions | undefined | null, callback?: (info: ServerInfo) => unknown): ServerInfo
|
|
28
|
+
listen(callback?: (info: ServerInfo) => unknown): this
|
|
29
|
+
listen(options?: TcpServerOptions | undefined | null, callback?: (info: ServerInfo) => unknown): this
|
|
30
|
+
listen(port: number, callback?: (info: ServerInfo) => unknown): this
|
|
31
|
+
listen(port: number, host: string, callback?: (info: ServerInfo) => unknown): this
|
|
32
|
+
listen(port: number, host: string, backlog: number, callback?: (info: ServerInfo) => unknown): this
|
|
33
|
+
listen(port: number, backlog: number, callback?: (info: ServerInfo) => unknown): this
|
|
34
|
+
close(callback?: () => unknown): this
|
|
35
|
+
stop(): void
|
|
36
|
+
info(): ServerInfo | null
|
|
37
|
+
address(): { address: string, family: 'IPv4' | 'IPv6', port: number } | null
|
|
38
|
+
connections(): Array<IoUringTcpConnection>
|
|
39
|
+
getConnections(callback: (err: Error | null, count: number) => unknown): this
|
|
40
|
+
sendBatch(sends: Array<IoUringTcpBatchSend>): boolean
|
|
41
|
+
sendBatchAndClose(sends: Array<IoUringTcpBatchSend>): boolean
|
|
42
|
+
ref(): this
|
|
43
|
+
unref(): this
|
|
44
|
+
on(event: 'connection', listener: (connection: IoUringTcpConnection) => void): this
|
|
45
|
+
on(event: 'listening', listener: (info: ServerInfo) => void): this
|
|
46
|
+
on(event: 'data', listener: (connection: IoUringTcpConnection, data: Buffer) => void): this
|
|
47
|
+
on(event: 'connectionClose', listener: (connection: IoUringTcpConnection) => void): this
|
|
48
|
+
on(event: 'close', listener: () => void): this
|
|
49
|
+
on(event: string | symbol, listener: (...args: Array<any>) => void): this
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export declare function createTcpServer(
|
|
53
|
+
options?: TcpServerOptions | undefined | null,
|
|
54
|
+
connectionListener?: (connection: IoUringTcpConnection) => unknown
|
|
55
|
+
): IoUringTcpTransportServer
|
|
56
|
+
export declare function createTcpServer(
|
|
57
|
+
connectionListener?: (connection: IoUringTcpConnection) => unknown
|
|
58
|
+
): IoUringTcpTransportServer
|
|
59
|
+
|
|
60
|
+
export type IoUringTcpBatchSend =
|
|
61
|
+
| {
|
|
62
|
+
connection: IoUringTcpConnection
|
|
63
|
+
connectionId?: never
|
|
64
|
+
data: Buffer | string | Uint8Array
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
connection?: never
|
|
68
|
+
connectionId: number
|
|
69
|
+
data: Buffer | string | Uint8Array
|
|
70
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const nativeBinding = require('./native');
|
|
4
|
+
const tcpTransport = require('./tcp-transport')(nativeBinding.UringTcpServer);
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
...nativeBinding,
|
|
8
|
+
IoUringTcpConnection: tcpTransport.IoUringTcpConnection,
|
|
9
|
+
IoUringTcpTransportServer: tcpTransport.IoUringTcpTransportServer,
|
|
10
|
+
createTcpServer: tcpTransport.createTcpServer
|
|
11
|
+
};
|