Skip to content

Commit 5c22800

Browse files
committed
bench: add websockets
1 parent 9eb2a2f commit 5c22800

7 files changed

Lines changed: 420 additions & 19 deletions

File tree

benchmarks/_util/index.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,20 @@ function printResults (results) {
5050
return console.table(rows)
5151
}
5252

53-
module.exports = { makeParallelRequests, printResults }
53+
/**
54+
* @param {number} num
55+
* @returns {string}
56+
*/
57+
function formatBytes (num) {
58+
if (!Number.isFinite(num)) {
59+
throw new Error('invalid number')
60+
}
61+
62+
const prefixes = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']
63+
64+
const idx = Math.min(Math.floor(Math.log(num) / Math.log(1024)), prefixes.length - 1)
65+
66+
return `${(num / Math.pow(1024, idx)).toFixed(2)}${prefixes[idx]}`
67+
}
68+
69+
module.exports = { makeParallelRequests, printResults, formatBytes }

benchmarks/_util/runner.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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 {{ minSamples?: number; maxSamples?: number }} [options]
59+
* @returns {Promise<{ name: string; average: number; samples: number; fn: BenchMarkHandler; iterationPerSecond: number; 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; iterationPerSecond: number; 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+
for (let i = 0; i < 8; ++i) {
83+
// warmup
84+
await new Promise((resolve, reject) => {
85+
const info = new Info(name, resolve)
86+
87+
try {
88+
const p = fn(info)
89+
90+
waitMaybePromiseLike(p).catch((err) => reject(err))
91+
} catch (err) {
92+
reject(err)
93+
}
94+
})
95+
}
96+
97+
let timing = 0
98+
const minSamples = options.minSamples ?? 128
99+
100+
for (let j = 0; (j < minSamples || timing < 800_000_000) && (typeof options.maxSamples === 'number' ? options.maxSamples > j : true); ++j) {
101+
let resolve = (value) => {}
102+
let reject = (reason) => {}
103+
const promise = new Promise(
104+
(_resolve, _reject) => { resolve = _resolve; reject = _reject }
105+
)
106+
107+
const info = new Info(name, resolve)
108+
109+
try {
110+
const p = fn(info)
111+
112+
await waitMaybePromiseLike(p)
113+
} catch (err) {
114+
reject(err)
115+
}
116+
117+
await promise
118+
119+
samples.push({ time: info.diff() })
120+
121+
timing += info.diff()
122+
}
123+
124+
const average =
125+
samples.map((v) => v.time).reduce((a, b) => a + b, 0) / samples.length
126+
127+
results.push({
128+
name: names[i],
129+
average,
130+
samples: samples.length,
131+
fn,
132+
iterationPerSecond: 1e9 / average,
133+
min: samples.reduce((a, acc) => Math.min(a, acc.time), samples[0].time),
134+
max: samples.reduce((a, acc) => Math.max(a, acc.time), samples[0].time)
135+
})
136+
}
137+
138+
return results
139+
}
140+
141+
module.exports = { bench }

benchmarks/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"node-fetch": "^3.3.2",
2222
"request": "^2.88.2",
2323
"superagent": "^10.0.0",
24-
"wait-on": "^8.0.0"
24+
"wait-on": "^8.0.0",
25+
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.52.0"
2526
}
2627
}

benchmarks/websocket-benchmark.mjs

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// @ts-check
2+
3+
import { bench } from './_util/runner.js'
4+
import { formatBytes } from './_util/index.js'
5+
import { WebSocket, WebSocketStream } from '../index.js'
6+
import { WebSocket as WsWebSocket } from 'ws'
7+
8+
/**
9+
* @type {Record<string, { fn: (ws: any, binary: string | Uint8Array) => import('./_util/runner.js').BenchMarkHandler; connect: (url: string) => Promise<any>; binaries: (string | Uint8Array)[] }>}
10+
*/
11+
const experiments = {}
12+
/**
13+
* @type {Record<string, { bytes: number; binaryType: 'string' | 'binary' }>}
14+
*/
15+
const experimentsInfo = {}
16+
17+
/**
18+
* @type {any[]}
19+
*/
20+
const connections = []
21+
22+
const binary = Buffer.alloc(256 * 1024, '_')
23+
const binaries = [binary, binary.subarray(0, 256 * 1024).toString('utf-8')]
24+
25+
experiments['undici'] = {
26+
fn: (ws, binary) => {
27+
if (!(ws instanceof WebSocket)) {
28+
throw new Error("'undici' websocket are expected.")
29+
}
30+
31+
return (ev) => {
32+
ws.addEventListener(
33+
'message',
34+
() => {
35+
ev.end()
36+
},
37+
{ once: true }
38+
)
39+
40+
ev.start()
41+
ws.send(binary)
42+
}
43+
},
44+
45+
connect: async (url) => {
46+
const ws = new WebSocket(url)
47+
48+
await /** @type {Promise<void>} */ (
49+
new Promise((resolve, reject) => {
50+
function onOpen () {
51+
resolve()
52+
ws.removeEventListener('open', onOpen)
53+
ws.removeEventListener('error', onError)
54+
}
55+
function onError (err) {
56+
reject(err)
57+
ws.removeEventListener('open', onOpen)
58+
ws.removeEventListener('error', onError)
59+
}
60+
ws.addEventListener('open', onOpen)
61+
ws.addEventListener('error', onError)
62+
})
63+
)
64+
65+
// avoid create blob
66+
ws.binaryType = 'arraybuffer'
67+
68+
return ws
69+
},
70+
71+
binaries
72+
}
73+
74+
experiments['undici - stream'] = {
75+
fn: (ws, binary) => {
76+
/** @type {ReadableStreamDefaultReader<string | Uint8Array>} */
77+
const reader = ws.reader
78+
/** @type {WritableStreamDefaultWriter<string | BufferSource>} */
79+
const writer = ws.writer
80+
81+
return async (ev) => {
82+
ev.start()
83+
await writer.write(binary)
84+
await reader.read()
85+
ev.end()
86+
}
87+
},
88+
89+
connect: async (url) => {
90+
const ws = new WebSocketStream(url)
91+
92+
const { readable, writable } = await ws.opened
93+
const reader = readable.getReader()
94+
const writer = writable.getWriter()
95+
96+
// @ts-ignore
97+
return { reader, writer, close: () => ws.close() }
98+
},
99+
100+
binaries
101+
}
102+
103+
experiments['ws'] = {
104+
fn: (ws, binary) => {
105+
if (!(ws instanceof WsWebSocket)) {
106+
throw new Error("'ws' websocket are expected.")
107+
}
108+
109+
return (ev) => {
110+
ws.once('message', () => {
111+
ev.end()
112+
})
113+
ev.start()
114+
ws.send(binary)
115+
}
116+
},
117+
118+
connect: async (url) => {
119+
const ws = new WsWebSocket(url, { maxPayload: 1024 * 1024 * 1024 })
120+
121+
await /** @type {Promise<void>} */ (
122+
new Promise((resolve, reject) => {
123+
function onOpen () {
124+
resolve()
125+
ws.off('open', onOpen)
126+
ws.off('error', onError)
127+
}
128+
function onError (err) {
129+
reject(err)
130+
ws.off('open', onOpen)
131+
ws.off('error', onError)
132+
}
133+
ws.on('open', onOpen)
134+
ws.on('error', onError)
135+
})
136+
)
137+
138+
ws.binaryType = 'arraybuffer'
139+
140+
return ws
141+
},
142+
143+
binaries
144+
}
145+
146+
async function init () {
147+
/** @type {Record<string, import('./_util/runner.js').BenchMarkHandler>} */
148+
const round = {}
149+
150+
const keys = Object.keys(experiments)
151+
152+
for (let i = 0; i < keys.length; ++i) {
153+
const name = keys[i]
154+
155+
const { fn, connect, binaries } = experiments[name]
156+
157+
const ws = await connect('ws://localhost:8080')
158+
159+
const needShowBytes = binaries.length !== 2 || typeof binaries[0] === typeof binaries[1]
160+
for (let i = 0; i < binaries.length; ++i) {
161+
const binary = binaries[i]
162+
const bytes = Buffer.byteLength(binary)
163+
164+
const binaryType = typeof binary === 'string' ? 'string' : 'binary'
165+
const roundName = needShowBytes
166+
? `${name} [${formatBytes(bytes)} (${binaryType})]`
167+
: `${name} [${binaryType}]`
168+
169+
round[roundName] = fn(ws, binary)
170+
experimentsInfo[roundName] = { bytes, binaryType }
171+
}
172+
173+
connections.push(ws)
174+
}
175+
176+
return round
177+
}
178+
179+
init()
180+
.then((round) => bench(round, {
181+
minSamples: 512
182+
}))
183+
.then((results) => {
184+
print(results)
185+
186+
for (const ws of connections) {
187+
ws.close()
188+
}
189+
}, (err) => {
190+
process.nextTick((err) => {
191+
throw err
192+
}, err)
193+
})
194+
195+
/**
196+
* @param {{ name: string; average: number; iterationPerSecond: number; }[]} results
197+
*/
198+
function print (results) {
199+
for (const { name, average, iterationPerSecond } of results) {
200+
const { bytes } = experimentsInfo[name]
201+
202+
console.log(
203+
`${name}: transferred ${formatBytes((bytes / average) * 1e9)} Bytes/s (${iterationPerSecond.toFixed(4)} per/sec)`
204+
)
205+
}
206+
}
207+
208+
export {}

0 commit comments

Comments
 (0)