Skip to content

Commit 2d4b1f8

Browse files
committed
update
1 parent bd461ae commit 2d4b1f8

File tree

6 files changed

+236
-36
lines changed

6 files changed

+236
-36
lines changed

bench.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,4 @@ await run()
7676

7777
for (const ws of connections) {
7878
ws.close()
79-
}
79+
}

benchmarks/_util/runner.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//@ts-check
2+
3+
"use strict";
4+
5+
class Info {
6+
/**@type {string} */
7+
#name;
8+
/**@type {bigint} */
9+
#current;
10+
/**@type {bigint} */
11+
#finish;
12+
/**@type {(...args: any[]) => any} */
13+
#callback;
14+
/**@type {boolean} */
15+
#finalized = false;
16+
17+
/**
18+
* @param {string} name
19+
* @param {(...args: any[]) => any} callback
20+
*/
21+
constructor(name, callback) {
22+
this.#name = name;
23+
this.#callback = callback;
24+
}
25+
26+
get name() {
27+
return this.#name;
28+
}
29+
30+
start() {
31+
if (this.#finalized) {
32+
throw new TypeError("called after finished.");
33+
}
34+
this.#current = process.hrtime.bigint();
35+
}
36+
37+
end() {
38+
if (this.#finalized) {
39+
throw new TypeError("called after finished.");
40+
}
41+
this.#finish = process.hrtime.bigint();
42+
this.#finalized = true;
43+
this.#callback();
44+
}
45+
46+
diff() {
47+
return Number(this.#finish - this.#current);
48+
}
49+
}
50+
51+
/**
52+
* @typedef BenchMarkHandler
53+
* @type {(ev: { name: string; start(): void; end(): void; }) => any}
54+
*/
55+
56+
/**
57+
* @param {Record<string, BenchMarkHandler>} experiments
58+
* @param {{}} [options]
59+
* @returns {Promise<{ name: string; average: number; samples: number; fn: BenchMarkHandler; min: number; max: number }[]>}
60+
*/
61+
async function bench(experiments, options = {}) {
62+
const names = Object.keys(experiments);
63+
64+
/**@type {{ name: string; average: number; samples: number; fn: BenchMarkHandler; min: number; max: number }[]} */
65+
const results = [];
66+
67+
async function waitMaybePromiseLike(p) {
68+
if (
69+
(typeof p === "object" || typeof p === "function") &&
70+
p !== null &&
71+
typeof p.then === "function"
72+
) {
73+
await p;
74+
}
75+
}
76+
77+
for (let i = 0; i < names.length; ++i) {
78+
const name = names[i];
79+
const fn = experiments[name];
80+
const samples = [];
81+
82+
let timing = 0;
83+
84+
for (let j = 0; j < 128 || timing < 800_000_000; ++j) {
85+
let resolve = (value) => {},
86+
reject = (reason) => {},
87+
promise = new Promise(
88+
(done, fail) => ((resolve = done), (reject = fail))
89+
);
90+
91+
const info = new Info(name, resolve);
92+
93+
try {
94+
const p = fn(info);
95+
96+
await waitMaybePromiseLike(p);
97+
} catch (err) {
98+
reject(err);
99+
}
100+
101+
await promise;
102+
103+
samples.push({ time: 1e6 * info.diff() });
104+
105+
timing += info.diff();
106+
}
107+
108+
const average =
109+
samples.map((v) => v.time).reduce((a, b) => a + b, 0) / samples.length;
110+
111+
results.push({
112+
name: names[i],
113+
average: average,
114+
samples: samples.length,
115+
fn: fn,
116+
min: samples.reduce((a, acc) => Math.min(a, acc.time), samples[0].time),
117+
max: samples.reduce((a, acc) => Math.max(a, acc.time), samples[0].time),
118+
});
119+
}
120+
121+
return results;
122+
}
123+
124+
function print() {
125+
126+
}
127+
128+
module.exports = { bench };

benchmarks/_util/websocket-simple-server.js

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,6 @@ const stream = require('node:stream')
66

77
const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
88

9-
function safeBufferConcat (chunks, length) {
10-
if (length === undefined) {
11-
length = 0
12-
for (let i = 0; i < chunks.length; ++i) {
13-
length += chunks[i].length
14-
}
15-
}
16-
const buffer = Buffer.allocUnsafeSlow(length)
17-
18-
let offset = 0
19-
for (let i = 0; i < chunks.length; ++i) {
20-
const chunk = chunks[i]
21-
buffer.set(chunk, offset)
22-
offset += chunk.length
23-
}
24-
25-
if (length !== offset) {
26-
buffer.fill(0, offset, buffer.length)
27-
}
28-
29-
return buffer
30-
}
31-
329
class ws {
3310
/**
3411
* @param {number} opcode
@@ -72,10 +49,10 @@ class ws {
7249
}
7350

7451
/**
75-
* @param {Uint8Array} buffer
52+
* @param {Uint8Array | null} buffer
7653
*/
7754
static parseFrame (buffer) {
78-
if (buffer.length < 2) {
55+
if (buffer === null || buffer.length < 2) {
7956
return null
8057
}
8158

@@ -125,6 +102,7 @@ class ws {
125102
}
126103

127104
static Stream = class extends stream.Writable {
105+
/** @type {Uint8Array | null} */
128106
#head = null
129107
#receivedLength = 0
130108

@@ -133,7 +111,7 @@ class ws {
133111
if (this.#head === null) {
134112
this.#head = chunk
135113
} else {
136-
this.#head = safeBufferConcat([this.#head, chunk])
114+
this.#head = Buffer.concat([this.#head, chunk])
137115
}
138116
const head = this.#head
139117
const parsed = ws.parseFrame(head)
@@ -172,7 +150,7 @@ class ws {
172150
if (this.#head === null) {
173151
this.#head = chunk
174152
} else if (this.#head.length < 2) {
175-
this.#head = safeBufferConcat([this.#head, chunk])
153+
this.#head = Buffer.concat([this.#head, chunk])
176154
merged = true
177155
} else {
178156
this.#receivedLength += chunk.length
@@ -187,7 +165,7 @@ class ws {
187165
const start = length - (parsed.offset + parsed.length)
188166
if (chunk.length < start) {
189167
if (merged) throw new Error('fatal error')
190-
this.#head = safeBufferConcat([this.#head, chunk]).subarray(
168+
this.#head = Buffer.concat([this.#head, chunk]).subarray(
191169
start
192170
)
193171
} else {
@@ -268,21 +246,21 @@ class ws {
268246

269247
writeFrame (frame) {
270248
if (this.#socket.writable) {
271-
return new Promise((resolve, reject) => {
249+
return /** @type {Promise<void>} */(new Promise((resolve, reject) => {
272250
this.#socket.write(frame, (err) => {
273251
if (err) {
274252
reject(err)
275253
} else {
276254
resolve()
277255
}
278256
})
279-
})
257+
}))
280258
}
281259
}
282260

283261
async close () {
284262
if (this.#socket.writable) {
285-
await this.writeFrame(ws.createFrame(ws.opcode.CLOSE))
263+
await this.writeFrame(ws.createFrame(ws.opcode.CLOSE, new Uint8Array(0)))
286264
this.#socket.end()
287265
}
288266
}
@@ -297,7 +275,9 @@ class ws {
297275
*/
298276
function setup ({ onConnection, parseBody }) {
299277
const server = http.createServer((_req, res) => {
300-
res.end('')
278+
// Http handler
279+
res.writeHead(404)
280+
res.end('404 Not Found')
301281
})
302282

303283
server.on('upgrade', (req, socket, _head) => {

benchmarks/server-websocket.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import './websocket/server/simple.mjs'

benchmarks/websocket-benchmark.mjs

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,94 @@
1-
import { cronometro } from "cronometro"
1+
//@ts-check
22

3-
// TODO
3+
import { bench } from "./_util/runner.js";
4+
import { WebSocket } from "../index.js";
5+
import { randomBytes } from "node:crypto";
6+
const __BINARY_SIZE__ = 1024 * 256;
7+
8+
const binary = randomBytes(__BINARY_SIZE__);
9+
10+
/**
11+
* @param {{
12+
* send(buffer: Uint8Array | string): void;
13+
* addEventListener(name: string, listener: (...args: any) => void, options?: { once: boolean }): void
14+
* }} socket
15+
* @param {Uint8Array | string} data
16+
* @param {(...args: any) => any} callback
17+
*/
18+
function waitWrite(socket, data, callback) {
19+
return /** @type {Promise<void>} */(new Promise((resolve, reject) => {
20+
socket.send(data);
21+
22+
socket.addEventListener(
23+
"message",
24+
(ev) => {
25+
resolve();
26+
callback();
27+
},
28+
{ once: true }
29+
);
30+
}))
31+
}
32+
33+
/**@type {Record<string, import('./_util/runner.js').BenchMarkHandler} */
34+
const experiments = {};
35+
36+
/**@type {WebSocket | null} */
37+
let ws;
38+
39+
experiments["sending 256Kib (undici)"] = async function (ev) {
40+
if (ws === null) {
41+
throw new Error("called before initialized");
42+
}
43+
ev.start();
44+
await waitWrite(ws, binary, () => {
45+
ev.end();
46+
});
47+
};
48+
49+
async function connect() {
50+
ws = new WebSocket("ws://localhost:5001");
51+
52+
await /**@type {Promise<void>} */ (
53+
new Promise((resolve, reject) => {
54+
if (ws === null) {
55+
return void reject(new Error("called before initialized"));
56+
}
57+
function onOpen() {
58+
resolve();
59+
this.removeEventListener("open", onOpen);
60+
this.removeEventListener("error", onError);
61+
}
62+
function onError(err) {
63+
reject(err);
64+
this.removeEventListener("open", onOpen);
65+
this.removeEventListener("error", onError);
66+
}
67+
ws.addEventListener("open", onOpen);
68+
ws.addEventListener("error", onError);
69+
})
70+
);
71+
}
72+
73+
connect()
74+
.then(() => bench(experiments, {}))
75+
.then((results) => {
76+
if (ws === null) {
77+
throw new Error("called before initialized");
78+
}
79+
80+
ws.close();
81+
82+
console.log(results);
83+
})
84+
.catch((err) => {
85+
process.nextTick((err) => {
86+
throw err;
87+
}, err);
88+
});
89+
90+
function print(results) {
91+
92+
}
93+
94+
export {};

benchmarks/websocket/server/simple.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ if (cluster.isPrimary) {
88
cluster.fork()
99
}
1010
} else {
11-
const emptyFrame = ws.createFrame(ws.opcode.BINARY, Buffer.allocUnsafe(0))
11+
const emptyFrame = ws.createFrame(ws.opcode.BINARY, new Uint8Array(0))
1212

1313
const server = setup({
1414
onConnection (ctrl) {

0 commit comments

Comments
 (0)