-
-
Notifications
You must be signed in to change notification settings - Fork 660
bench: add websockets #3203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
bench: add websockets #3203
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #3203 +/- ##
=======================================
Coverage 94.17% 94.17%
=======================================
Files 90 90
Lines 24432 24432
=======================================
Hits 23009 23009
Misses 1423 1423 ☔ View full report in Codecov by Sentry. |
To have reliable benchmarks you should use two processes, one for the server and one for the client and ensure that the server is faster than the client, otherwise you might end up benchmarking the server instead of the client. See the discussion in nodejs/node#50586. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to block for the previous comments
7af0bac
to
7ef0e3a
Compare
f5204cf
to
370f8cb
Compare
370f8cb
to
943796a
Compare
2d4b1f8
to
6a6b336
Compare
841a95b
to
1444870
Compare
e249b3b
to
5e86207
Compare
18e2482
to
6f1e7ef
Compare
6f1e7ef
to
097cca7
Compare
fdcd74a
to
d70f6c7
Compare
> $ node ./benchmarks/websocket-benchmark.mjs
(node:9028) [UNDICI-WSS] Warning: WebSocketStream is experimental! Expect it to change at any time.
(Use `node --trace-warnings ...` to show where the warning was created)
undici [binary]: transferred 102.46MiB/s
undici [string]: transferred 99.43MiB/s
undici - stream [binary]: transferred 95.38MiB/s
undici - stream [string]: transferred 86.72MiB/s
ws [binary]: transferred 100.69MiB/s
ws [string]: transferred 95.50MiB/s |
@tsctx I'm a bit skeptical about the results // server.js
const uws = require('uWebSockets.js');
const app = uws.App();
app.ws('/*', {
compression: uws.DISABLED,
maxPayloadLength: 512 * 1024 * 1024,
maxBackpressure: 128 * 1024,
message: (ws, message, isBinary) => {
ws.send(message, isBinary);
}
});
app.listen(8080, (listenSocket) => {
if (listenSocket) {
console.log('Server listening to port 8080');
}
}); // ws-client.js
'use strict';
const { WebSocket } = require('ws');
const messages = +process.argv[2];
const payloadLength = +process.argv[3];
const data = Buffer.alloc(payloadLength, '_');
const ws = new WebSocket('ws://127.0.0.1:8080');
ws.binaryType = 'arraybuffer';
ws.on('open', function () {
console.time(`${messages} messages of ${payloadLength} bytes`);
ws.send(data);
});
let count = 0;
ws.on('message', function () {
if (++count === messages) {
console.timeEnd(`${messages} messages of ${payloadLength} bytes`);
ws.close();
} else {
ws.send(data);
}
}); // undici-client.js
'use strict';
const messages = +process.argv[2];
const payloadLength = +process.argv[3];
const data = Buffer.alloc(payloadLength, '_');
const ws = new WebSocket('ws://127.0.0.1:8080');
ws.binaryType = 'arraybuffer';
ws.addEventListener('open', function () {
console.time(`${messages} messages of ${payloadLength} bytes`);
ws.send(data);
});
let count = 0;
ws.addEventListener('message', function () {
if (++count === messages) {
console.timeEnd(`${messages} messages of ${payloadLength} bytes`);
ws.close();
} else {
ws.send(data);
}
});
This is without binary addons. |
Same here, but I'm happily surprised undici is that close in both benchmarks (other than very large messages). |
actually, masking seem to be the performance bottleneck, which I could improve based on this benchmarks... |
diff --git a/benchmarks/websocket/generate-mask.mjs b/benchmarks/websocket/generate-mask.mjs
index 032f05d8..c74cab08 100644
--- a/benchmarks/websocket/generate-mask.mjs
+++ b/benchmarks/websocket/generate-mask.mjs
@@ -1,20 +1,8 @@
-import { randomFillSync, randomBytes } from 'node:crypto'
+import { randomBytes } from 'node:crypto'
import { bench, group, run } from 'mitata'
+import { generateMask } from "../../lib/web/websocket/frame.js"
-const BUFFER_SIZE = 16384
-
-const buf = Buffer.allocUnsafe(BUFFER_SIZE)
-let bufIdx = BUFFER_SIZE
-
-function generateMask () {
- if (bufIdx === BUFFER_SIZE) {
- bufIdx = 0
- randomFillSync(buf, 0, BUFFER_SIZE)
- }
- return [buf[bufIdx++], buf[bufIdx++], buf[bufIdx++], buf[bufIdx++]]
-}
-
-group('generate', () => {
+group(function () {
bench('generateMask', () => generateMask())
bench('crypto.randomBytes(4)', () => randomBytes(4))
})
diff --git a/lib/web/websocket/frame.js b/lib/web/websocket/frame.js
index e773b33e..c0b5d779 100644
--- a/lib/web/websocket/frame.js
+++ b/lib/web/websocket/frame.js
@@ -4,6 +4,8 @@ const { maxUnsigned16Bit, opcodes } = require('./constants')
const BUFFER_SIZE = 8 * 1024
+const FIN = /** @type {const} */ (0x80)
+
/** @type {import('crypto')} */
let crypto
let buffer = null
@@ -59,10 +61,7 @@ class WebsocketFrameSend {
const buffer = Buffer.allocUnsafe(bodyLength + offset)
- // Clear first 2 bytes, everything else is overwritten
- buffer[0] = buffer[1] = 0
- buffer[0] |= 0x80 // FIN
- buffer[0] = (buffer[0] & 0xF0) + opcode // opcode
+ buffer[0] = FIN + opcode
/*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */
buffer[offset - 4] = maskKey[0]
@@ -70,21 +69,38 @@ class WebsocketFrameSend {
buffer[offset - 2] = maskKey[2]
buffer[offset - 1] = maskKey[3]
- buffer[1] = payloadLength
+ buffer[1] = payloadLength | 0x80
- if (payloadLength === 126) {
- buffer.writeUInt16BE(bodyLength, 2)
- } else if (payloadLength === 127) {
- // Clear extended payload length
- buffer[2] = buffer[3] = 0
- buffer.writeUIntBE(bodyLength, 4, 6)
+ if (payloadLength > 125) {
+ if (payloadLength === 126) {
+ buffer.writeUInt16BE(bodyLength, 2)
+ } else if (payloadLength === 127) {
+ // Clear extended payload length
+ buffer[2] = buffer[3] = 0
+ buffer.writeUIntBE(bodyLength, 4, 6)
+ }
}
- buffer[1] |= 0x80 // MASK
+ const rest = bodyLength & 3
+ const p4 = bodyLength - rest
+ let i = 0
// mask body
- for (let i = 0; i < bodyLength; ++i) {
- buffer[offset + i] = frameData[i] ^ maskKey[i & 3]
+ while (i < p4) {
+ buffer[offset + i] = frameData[i++] ^ maskKey[0]
+ buffer[offset + i] = frameData[i++] ^ maskKey[1]
+ buffer[offset + i] = frameData[i++] ^ maskKey[2]
+ buffer[offset + i] = frameData[i++] ^ maskKey[3]
+ i += 4
+ }
+
+ switch (rest) {
+ case 3:
+ buffer[offset + i + 2] = frameData[i + 2] ^ maskKey[2]
+ case 2:
+ buffer[offset + i + 1] = frameData[i + 1] ^ maskKey[1]
+ case 1:
+ buffer[offset + i] = frameData[i] ^ maskKey[0]
}
return buffer
@@ -134,5 +150,6 @@ class WebsocketFrameSend {
}
module.exports = {
+ generateMask,
WebsocketFrameSend
}
|
3bb9ada
to
09b62cf
Compare
@lpinca > $ node ws-client.js 100000 262144
6324 req/10s
6684 req/10s
6525 req/10s
5947 req/10s
6365 req/10s
6328 req/10s
6487 req/10s
6177 req/10s
6482 req/10s
5969 req/10s
6128 req/10s
6677 req/10s
6531 req/10s
6798 req/10s
6332 req/10s
100000 messages of 262144 bytes: 2:36.519 (m:ss.mmm)
> $ node undici-client.js 100000 262144
5849 req/10s
6128 req/10s
6476 req/10s
6570 req/10s
6649 req/10s
6599 req/10s
5920 req/10s
5633 req/10s
5625 req/10s
6377 req/10s
6377 req/10s
6413 req/10s
6403 req/10s
6429 req/10s
6391 req/10s
100000 messages of 262144 bytes: 2:39.189 (m:ss.mmm) Script// ws-client.js
'use strict'
const { WebSocket } = require('ws')
const messages = +process.argv[2]
const payloadLength = +process.argv[3]
const data = Buffer.alloc(payloadLength, '_')
const ws = new WebSocket('ws://127.0.0.1:8080')
ws.binaryType = 'arraybuffer'
let n = 0
let timer
ws.on('open', function () {
console.time(`${messages} messages of ${payloadLength} bytes`)
timer = setInterval(() => {
console.log(`${count - n} req/10s`)
n = count
}, 10000)
ws.send(data)
})
let count = 0
ws.on('message', function () {
if (++count === messages) {
console.timeEnd(`${messages} messages of ${payloadLength} bytes`)
ws.close()
clearInterval(timer)
} else {
ws.send(data)
}
}) // undici-client.js
'use strict'
const messages = +process.argv[2]
const payloadLength = +process.argv[3]
const data = Buffer.alloc(payloadLength, '_')
const { WebSocket } = require('./index')
const ws = new WebSocket('ws://127.0.0.1:8080')
ws.binaryType = 'arraybuffer'
let n = 0
let timer
ws.addEventListener('open', function () {
console.time(`${messages} messages of ${payloadLength} bytes`)
timer = setInterval(() => {
console.log(`${count - n} req/10s`)
n = count
}, 10000)
ws.send(data)
})
let count = 0
ws.addEventListener('message', function () {
if (++count === messages) {
console.timeEnd(`${messages} messages of ${payloadLength} bytes`)
ws.close()
clearInterval(timer)
} else {
ws.send(data)
}
}) // server.js
const uws = require('uWebSockets.js')
const app = uws.App()
app.ws('/*', {
compression: uws.DISABLED,
maxPayloadLength: 512 * 1024 * 1024,
maxBackpressure: 128 * 1024,
message: (ws, message, isBinary) => {
ws.send(message, isBinary)
}
})
app.listen(8080, (listenSocket) => {
if (listenSocket) {
console.log('Server listening to port 8080')
}
}) |
478578c
to
5c22800
Compare
5c22800
to
8e4991b
Compare
FWIW, results are similar to |
When we ran the benchmark, the global version of undici was older, so its performance didn't reflect the latest results. Now, we've updated to the most recent version, and its performance is comparable. |
* @param {number} num | ||
* @returns {string} | ||
*/ | ||
function formatBytes (num) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
love this implementation 🌟
let resolve = (value) => {} | ||
let reject = (reason) => {} | ||
const promise = new Promise( | ||
(_resolve, _reject) => { resolve = _resolve; reject = _reject } | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let resolve = (value) => {} | |
let reject = (reason) => {} | |
const promise = new Promise( | |
(_resolve, _reject) => { resolve = _resolve; reject = _reject } | |
) | |
const { promise, resolve, reject } = Promise.withResolvers(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this might only be valid Node v22+
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still support v20, so let's keep it that way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
I'm glad these are almost equal.
@KhafraDev ptal |
Part of #3201