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,178 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const http = require('node:http');
5
+ const path = require('node:path');
6
+ const { performance } = require('node:perf_hooks');
7
+ const { UringHttpServer } = require('../');
8
+
9
+ const BODY = 'ok\n';
10
+ const DURATION_MS = Number(process.env.DURATION_MS || 5000);
11
+ const CONCURRENCY = Number(process.env.CONCURRENCY || 128);
12
+ const QUEUE_DEPTH = Number(process.env.QUEUE_DEPTH || 256);
13
+ const REPORT_PATH = process.env.REPORT_PATH;
14
+
15
+ function requestOnce(port) {
16
+ const startedAt = performance.now();
17
+ return new Promise((resolve, reject) => {
18
+ const req = http.get({ host: '127.0.0.1', port, path: '/', agent: false }, (res) => {
19
+ res.resume();
20
+ res.on('end', () => resolve(performance.now() - startedAt));
21
+ });
22
+ req.on('error', reject);
23
+ });
24
+ }
25
+
26
+ async function runLoad(port) {
27
+ let completed = 0;
28
+ let stopped = false;
29
+ const latencies = [];
30
+ const endAt = performance.now() + DURATION_MS;
31
+
32
+ async function worker() {
33
+ while (!stopped) {
34
+ latencies.push(await requestOnce(port));
35
+ completed += 1;
36
+ if (performance.now() >= endAt) stopped = true;
37
+ }
38
+ }
39
+
40
+ await Promise.all(Array.from({ length: CONCURRENCY }, worker));
41
+ latencies.sort((a, b) => a - b);
42
+
43
+ return {
44
+ requests: completed,
45
+ rps: Math.round((completed * 1000) / DURATION_MS),
46
+ p50Ms: percentile(latencies, 0.5),
47
+ p95Ms: percentile(latencies, 0.95),
48
+ p99Ms: percentile(latencies, 0.99)
49
+ };
50
+ }
51
+
52
+ function percentile(values, quantile) {
53
+ if (values.length === 0) return 0;
54
+ const index = Math.min(values.length - 1, Math.floor(values.length * quantile));
55
+ return Number(values[index].toFixed(3));
56
+ }
57
+
58
+ async function withNodeServer() {
59
+ const server = http.createServer((_, res) => {
60
+ res.setHeader('connection', 'close');
61
+ res.end(BODY);
62
+ });
63
+
64
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
65
+ const port = server.address().port;
66
+ try {
67
+ return await runLoad(port);
68
+ } finally {
69
+ await new Promise((resolve) => server.close(resolve));
70
+ }
71
+ }
72
+
73
+ async function withUringServer() {
74
+ const server = new UringHttpServer({
75
+ host: '127.0.0.1',
76
+ port: 0,
77
+ queueDepth: QUEUE_DEPTH,
78
+ responseBody: BODY,
79
+ bufferCount: 4096,
80
+ bufferSize: 2048
81
+ });
82
+
83
+ const info = server.start();
84
+ try {
85
+ return {
86
+ ...(await runLoad(info.port)),
87
+ serverInfo: summarizeServerInfo(server.info())
88
+ };
89
+ } finally {
90
+ server.stop();
91
+ }
92
+ }
93
+
94
+ function summarizeServerInfo(info) {
95
+ if (!info) return null;
96
+ return {
97
+ backend: info.backend,
98
+ backlog: info.backlog,
99
+ queueDepth: info.queueDepth,
100
+ bufferCount: info.bufferCount,
101
+ bufferSize: info.bufferSize,
102
+ tcpNoDelay: info.tcpNoDelay,
103
+ reusePort: info.reusePort,
104
+ tcpDeferAcceptSeconds: info.tcpDeferAcceptSeconds,
105
+ socketRecvBufferSize: info.socketRecvBufferSize,
106
+ socketSendBufferSize: info.socketSendBufferSize,
107
+ multishotAccept: info.multishotAccept,
108
+ multishotRecv: info.multishotRecv,
109
+ providedBufferRing: info.providedBufferRing,
110
+ recvBundle: info.recvBundle,
111
+ recvCopyEvents: info.recvCopyEvents,
112
+ recvCopyBytes: info.recvCopyBytes,
113
+ eventBatchSize: info.eventBatchSize,
114
+ sendBufferCount: info.sendBufferCount,
115
+ sendBufferSize: info.sendBufferSize,
116
+ registeredSendBuffer: info.registeredSendBuffer,
117
+ fixedSendBufferMisses: info.fixedSendBufferMisses,
118
+ fixedSendBufferMissBytes: info.fixedSendBufferMissBytes,
119
+ zeroCopySend: info.zeroCopySend,
120
+ zeroCopyReceive: info.zeroCopyReceive,
121
+ zcrxReady: info.zcrxReady
122
+ };
123
+ }
124
+
125
+ function baseReport() {
126
+ return {
127
+ mode: 'http-fixed-response',
128
+ status: 'running',
129
+ startedAt: new Date().toISOString(),
130
+ finishedAt: null,
131
+ config: {
132
+ durationMs: DURATION_MS,
133
+ concurrency: CONCURRENCY,
134
+ queueDepth: QUEUE_DEPTH
135
+ },
136
+ results: [],
137
+ error: null
138
+ };
139
+ }
140
+
141
+ function writeReport(report) {
142
+ if (!REPORT_PATH) return;
143
+ fs.mkdirSync(path.dirname(REPORT_PATH), { recursive: true });
144
+ fs.writeFileSync(REPORT_PATH, `${JSON.stringify(report, null, 2)}\n`);
145
+ console.log(`HTTP benchmark report written: ${REPORT_PATH}`);
146
+ }
147
+
148
+ function errorForReport(error) {
149
+ return {
150
+ name: error && error.name ? error.name : 'Error',
151
+ message: error && error.message ? error.message : String(error),
152
+ stack: error && error.stack ? error.stack : undefined
153
+ };
154
+ }
155
+
156
+ (async () => {
157
+ const report = baseReport();
158
+ try {
159
+ console.log(report.config);
160
+ const nodeResult = await withNodeServer();
161
+ report.results.push({ caseName: 'node:http', result: nodeResult });
162
+ console.log('node:http', nodeResult);
163
+ const ferringsResult = await withUringServer();
164
+ report.results.push({ caseName: 'ferrings', result: ferringsResult });
165
+ console.log('ferrings', ferringsResult);
166
+ report.status = 'passed';
167
+ } catch (error) {
168
+ report.status = 'failed';
169
+ report.error = errorForReport(error);
170
+ throw error;
171
+ } finally {
172
+ report.finishedAt = new Date().toISOString();
173
+ writeReport(report);
174
+ }
175
+ })().catch((error) => {
176
+ console.error(error);
177
+ process.exitCode = 1;
178
+ });
@@ -0,0 +1,232 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+ const { capabilities } = require('../');
8
+
9
+ const DURATION_MS = String(process.env.DURATION_MS || 1000);
10
+ const CONCURRENCY = String(process.env.CONCURRENCY || 128);
11
+ const QUEUE_DEPTH = String(process.env.QUEUE_DEPTH || 256);
12
+ const SYSCALL_REQUESTS = String(process.env.SYSCALL_REQUESTS || 200);
13
+ const SYSCALL_CONCURRENCY = String(process.env.SYSCALL_CONCURRENCY || 32);
14
+ const SYSCALL_CASES =
15
+ process.env.SYSCALL_CASES ||
16
+ 'node-http,ferrings-http,node-tcp,ferrings-tcp-facade,ferrings-tcp-facade-batch,ferrings-native-tcp';
17
+ const REPORT_PATH = process.env.REPORT_PATH;
18
+
19
+ const report = {
20
+ mode: 'first-slice',
21
+ status: 'running',
22
+ startedAt: new Date().toISOString(),
23
+ finishedAt: null,
24
+ config: {
25
+ durationMs: Number(DURATION_MS),
26
+ concurrency: Number(CONCURRENCY),
27
+ queueDepth: Number(QUEUE_DEPTH),
28
+ syscallRequests: Number(SYSCALL_REQUESTS),
29
+ syscallConcurrency: Number(SYSCALL_CONCURRENCY),
30
+ syscallCases: SYSCALL_CASES.split(',').map((name) => name.trim()).filter(Boolean)
31
+ },
32
+ capabilities: capabilities(),
33
+ results: [],
34
+ summary: {},
35
+ error: null
36
+ };
37
+
38
+ try {
39
+ console.log({ mode: report.mode, ...report.config });
40
+ report.results.push(
41
+ runBenchmark('HTTP fixed response latency', 'compare.js', {
42
+ DURATION_MS,
43
+ CONCURRENCY,
44
+ QUEUE_DEPTH
45
+ })
46
+ );
47
+ report.results.push(
48
+ runBenchmark('TCP echo latency matrix', 'tcp-echo.js', {
49
+ DURATION_MS,
50
+ CONCURRENCY,
51
+ QUEUE_DEPTH
52
+ })
53
+ );
54
+ report.results.push(runSyscallBenchmark());
55
+ report.summary = buildSummary(report);
56
+ report.status = 'passed';
57
+ } catch (error) {
58
+ report.status = 'failed';
59
+ report.error = errorForReport(error);
60
+ if (error && error.childResult) {
61
+ report.results.push(error.childResult);
62
+ }
63
+ throw error;
64
+ } finally {
65
+ report.finishedAt = new Date().toISOString();
66
+ writeReport(report);
67
+ }
68
+
69
+ function runSyscallBenchmark() {
70
+ const strace = spawnSync('strace', ['-V'], { encoding: 'utf8' });
71
+ if (strace.error || strace.status !== 0) {
72
+ return {
73
+ label: 'Syscalls per connection',
74
+ script: 'syscalls.js',
75
+ status: 'skipped',
76
+ skippedReason: 'strace is not available',
77
+ report: null
78
+ };
79
+ }
80
+ return runBenchmark('Syscalls per connection', 'syscalls.js', {
81
+ REQUESTS: SYSCALL_REQUESTS,
82
+ CONCURRENCY: SYSCALL_CONCURRENCY,
83
+ QUEUE_DEPTH,
84
+ CASES: SYSCALL_CASES
85
+ });
86
+ }
87
+
88
+ function runBenchmark(label, script, extraEnv) {
89
+ console.log(`\n== ${label} ==`);
90
+ const childReportPath = path.join(
91
+ os.tmpdir(),
92
+ `ferrings-first-slice-${process.pid}-${path.basename(script, '.js')}-${Date.now()}.json`
93
+ );
94
+ const childResult = {
95
+ label,
96
+ script,
97
+ status: 'running',
98
+ report: null
99
+ };
100
+ const result = spawnSync(process.execPath, [path.join(__dirname, script)], {
101
+ cwd: path.join(__dirname, '..'),
102
+ env: {
103
+ ...process.env,
104
+ ...extraEnv,
105
+ REPORT_PATH: childReportPath
106
+ },
107
+ stdio: 'inherit'
108
+ });
109
+ childResult.report = readChildReport(childReportPath);
110
+ childResult.status = result.status === 0 ? 'passed' : 'failed';
111
+ if (result.error) {
112
+ result.error.childResult = childResult;
113
+ throw result.error;
114
+ }
115
+ if (result.status !== 0) {
116
+ const error = new Error(`${script} exited with status ${result.status ?? 1}`);
117
+ error.childResult = childResult;
118
+ throw error;
119
+ }
120
+ return childResult;
121
+ }
122
+
123
+ function readChildReport(childReportPath) {
124
+ try {
125
+ if (!fs.existsSync(childReportPath)) {
126
+ return null;
127
+ }
128
+ return JSON.parse(fs.readFileSync(childReportPath, 'utf8'));
129
+ } catch (error) {
130
+ return {
131
+ status: 'unreadable',
132
+ error: errorForReport(error)
133
+ };
134
+ } finally {
135
+ fs.rmSync(childReportPath, { force: true });
136
+ }
137
+ }
138
+
139
+ function buildSummary(output) {
140
+ const httpReport = childReport(output, 'compare.js');
141
+ const tcpReport = childReport(output, 'tcp-echo.js');
142
+ const syscallsReport = childReport(output, 'syscalls.js');
143
+ return {
144
+ httpLatency: compareResults(
145
+ resultByCase(httpReport, 'node:http'),
146
+ resultByCase(httpReport, 'ferrings')
147
+ ),
148
+ tcpNativeLatency: compareResults(
149
+ resultByCase(tcpReport, 'node:net echo'),
150
+ resultByCase(tcpReport, 'ferrings native tcp echo')
151
+ ),
152
+ tcpFacadeLatency: compareResults(
153
+ resultByCase(tcpReport, 'node:net echo'),
154
+ resultByCase(tcpReport, 'ferrings tcp facade echo')
155
+ ),
156
+ tcpFacadeBatchLatency: compareResults(
157
+ resultByCase(tcpReport, 'node:net echo'),
158
+ resultByCase(tcpReport, 'ferrings tcp facade batch echo')
159
+ ),
160
+ httpSyscalls: compareResults(
161
+ resultByCase(syscallsReport, 'node-http'),
162
+ resultByCase(syscallsReport, 'ferrings-http'),
163
+ 'syscallsPerConnection'
164
+ ),
165
+ tcpNativeSyscalls: compareResults(
166
+ resultByCase(syscallsReport, 'node-tcp'),
167
+ resultByCase(syscallsReport, 'ferrings-native-tcp'),
168
+ 'syscallsPerConnection'
169
+ ),
170
+ tcpFacadeSyscalls: compareResults(
171
+ resultByCase(syscallsReport, 'node-tcp'),
172
+ resultByCase(syscallsReport, 'ferrings-tcp-facade'),
173
+ 'syscallsPerConnection'
174
+ ),
175
+ tcpFacadeBatchSyscalls: compareResults(
176
+ resultByCase(syscallsReport, 'node-tcp'),
177
+ resultByCase(syscallsReport, 'ferrings-tcp-facade-batch'),
178
+ 'syscallsPerConnection'
179
+ )
180
+ };
181
+ }
182
+
183
+ function childReport(output, script) {
184
+ const entry = output.results.find((result) => result.script === script);
185
+ return entry && entry.report && entry.report.status === 'passed' ? entry.report : null;
186
+ }
187
+
188
+ function resultByCase(child, caseName) {
189
+ if (!child || !Array.isArray(child.results)) return null;
190
+ const entry = child.results.find((result) => result.caseName === caseName);
191
+ return entry ? entry.result : null;
192
+ }
193
+
194
+ function compareResults(baseline, candidate, metric = 'p99Ms') {
195
+ if (!baseline || !candidate) {
196
+ return null;
197
+ }
198
+ const baselineMetric = Number(baseline[metric]);
199
+ const candidateMetric = Number(candidate[metric]);
200
+ return {
201
+ baseline,
202
+ candidate,
203
+ metric,
204
+ baselineValue: baselineMetric,
205
+ candidateValue: candidateMetric,
206
+ delta: finiteNumber(candidateMetric - baselineMetric),
207
+ ratio: finiteNumber(candidateMetric / baselineMetric),
208
+ rpsRatio: finiteNumber(Number(candidate.rps) / Number(baseline.rps))
209
+ };
210
+ }
211
+
212
+ function finiteNumber(value) {
213
+ return Number.isFinite(value) ? Number(value.toFixed(3)) : null;
214
+ }
215
+
216
+ function writeReport(output) {
217
+ if (REPORT_PATH) {
218
+ fs.mkdirSync(path.dirname(REPORT_PATH), { recursive: true });
219
+ fs.writeFileSync(REPORT_PATH, `${JSON.stringify(output, null, 2)}\n`);
220
+ console.log(`first-slice benchmark report written: ${REPORT_PATH}`);
221
+ } else {
222
+ console.log(JSON.stringify(output.summary, null, 2));
223
+ }
224
+ }
225
+
226
+ function errorForReport(error) {
227
+ return {
228
+ name: error && error.name ? error.name : 'Error',
229
+ message: error && error.message ? error.message : String(error),
230
+ stack: error && error.stack ? error.stack : undefined
231
+ };
232
+ }
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const { spawnSync } = require('node:child_process');
6
+ const path = require('node:path');
7
+
8
+ const DEFAULT_DURATION_MS = '10000';
9
+ const DEFAULT_CONCURRENCY = '512';
10
+ const DEFAULT_QUEUE_DEPTH = '1024';
11
+ const REPORT_PATH = process.env.REPORT_PATH;
12
+
13
+ const env = {
14
+ ...process.env,
15
+ DURATION_MS: process.env.DURATION_MS || DEFAULT_DURATION_MS,
16
+ CONCURRENCY: process.env.CONCURRENCY || DEFAULT_CONCURRENCY,
17
+ QUEUE_DEPTH: process.env.QUEUE_DEPTH || DEFAULT_QUEUE_DEPTH
18
+ };
19
+
20
+ const report = {
21
+ mode: 'high-concurrency',
22
+ status: 'running',
23
+ startedAt: new Date().toISOString(),
24
+ finishedAt: null,
25
+ config: {
26
+ durationMs: Number(env.DURATION_MS),
27
+ concurrency: Number(env.CONCURRENCY),
28
+ queueDepth: Number(env.QUEUE_DEPTH)
29
+ },
30
+ results: [],
31
+ error: null
32
+ };
33
+
34
+ try {
35
+ console.log({ mode: report.mode, ...report.config });
36
+ report.results.push(run('HTTP fixed response', 'compare.js'));
37
+ report.results.push(run('TCP echo matrix', 'tcp-echo.js'));
38
+ report.status = 'passed';
39
+ } catch (error) {
40
+ report.status = 'failed';
41
+ report.error = errorForReport(error);
42
+ if (error && error.childResult) {
43
+ report.results.push(error.childResult);
44
+ }
45
+ throw error;
46
+ } finally {
47
+ report.finishedAt = new Date().toISOString();
48
+ writeReport(report);
49
+ }
50
+
51
+ function run(label, script) {
52
+ console.log(`\n== ${label} ==`);
53
+ const childResult = {
54
+ label,
55
+ script,
56
+ report: null
57
+ };
58
+ const childEnv = { ...env };
59
+ const childReportPath = REPORT_PATH
60
+ ? path.join(
61
+ os.tmpdir(),
62
+ `ferrings-high-${process.pid}-${path.basename(script, '.js')}-${Date.now()}.json`
63
+ )
64
+ : null;
65
+ if (childReportPath) {
66
+ childEnv.REPORT_PATH = childReportPath;
67
+ }
68
+ const result = spawnSync(
69
+ process.execPath,
70
+ [path.join(__dirname, script)],
71
+ {
72
+ cwd: path.join(__dirname, '..'),
73
+ env: childEnv,
74
+ stdio: 'inherit'
75
+ }
76
+ );
77
+ if (result.error) {
78
+ childResult.report = readChildReport(childReportPath);
79
+ result.error.childResult = childResult;
80
+ throw result.error;
81
+ }
82
+ childResult.report = readChildReport(childReportPath);
83
+ if (result.status !== 0) {
84
+ const error = new Error(`${script} exited with status ${result.status ?? 1}`);
85
+ error.childResult = childResult;
86
+ throw error;
87
+ }
88
+ return childResult;
89
+ }
90
+
91
+ function readChildReport(childReportPath) {
92
+ if (!childReportPath) return null;
93
+ if (!fs.existsSync(childReportPath)) return null;
94
+ try {
95
+ return JSON.parse(fs.readFileSync(childReportPath, 'utf8'));
96
+ } catch (error) {
97
+ return {
98
+ status: 'unreadable',
99
+ error: errorForReport(error)
100
+ };
101
+ } finally {
102
+ fs.rmSync(childReportPath, { force: true });
103
+ }
104
+ }
105
+
106
+ function writeReport(output) {
107
+ if (!REPORT_PATH) return;
108
+ fs.mkdirSync(path.dirname(REPORT_PATH), { recursive: true });
109
+ fs.writeFileSync(REPORT_PATH, `${JSON.stringify(output, null, 2)}\n`);
110
+ console.log(`high-concurrency benchmark report written: ${REPORT_PATH}`);
111
+ }
112
+
113
+ function errorForReport(error) {
114
+ return {
115
+ name: error && error.name ? error.name : 'Error',
116
+ message: error && error.message ? error.message : String(error),
117
+ stack: error && error.stack ? error.stack : undefined
118
+ };
119
+ }