From 5f7b47d40f9daabe4e3c321eda620bbadfe5ce96 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Fri, 6 Jan 2023 08:42:51 +0100 Subject: [PATCH] perf: precompute the WebSocket frames when broadcasting Note: - only packets without binary attachments are affected - the permessage-deflate extension must be disabled (which is the default) Previous attempt: - wsPreEncoded option: https://github.com/socketio/socket.io-adapter/commit/5579d40c24d15f69e44246f788fb93beb367f994 - fix for binary packets: https://github.com/socketio/socket.io-adapter/commit/a33e42bb7b935ccdd3688b4c305714b791ade0db - revert: https://github.com/socketio/socket.io-adapter/commit/88eee5948aba94f999405239025f29c754a002e2 --- lib/index.ts | 24 ++++++++++++++++++++++-- package-lock.json | 31 +++++++++++++++++++++++++++++++ package.json | 3 +++ test/index.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index 33c84b2..16fd818 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,5 +1,6 @@ import { EventEmitter } from "events"; import { yeast } from "./contrib/yeast"; +import { WebSocket } from "ws"; /** * A public ID, sent by the server at the beginning of the Socket.IO session and which can be used for private messaging @@ -164,7 +165,7 @@ export class Adapter extends EventEmitter { }; packet.nsp = this.nsp.name; - const encodedPackets = this.encoder.encode(packet); + const encodedPackets = this._encode(packet, packetOpts); this.apply(opts, (socket) => { if (typeof socket.notifyOutgoingListeners === "function") { @@ -207,7 +208,7 @@ export class Adapter extends EventEmitter { // we can use the same id for each packet, since the _ids counter is common (no duplicate) packet.id = this.nsp._ids++; - const encodedPackets = this.encoder.encode(packet); + const encodedPackets = this._encode(packet, packetOpts); let clientCount = 0; @@ -227,6 +228,25 @@ export class Adapter extends EventEmitter { clientCountCallback(clientCount); } + private _encode(packet: unknown, packetOpts: Record) { + const encodedPackets = this.encoder.encode(packet); + + if (encodedPackets.length === 1 && typeof encodedPackets[0] === "string") { + // "4" being the "message" packet type in the Engine.IO protocol + const data = Buffer.from("4" + encodedPackets[0]); + // see https://github.com/websockets/ws/issues/617#issuecomment-283002469 + packetOpts.wsPreEncodedFrame = WebSocket.Sender.frame(data, { + readOnly: false, + mask: false, + rsv1: false, + opcode: 1, + fin: true, + }); + } + + return encodedPackets; + } + /** * Gets a list of sockets by sid. * diff --git a/package-lock.json b/package-lock.json index bcb9c09..2d17734 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,9 @@ "prettier": "^2.8.1", "ts-node": "^10.9.1", "typescript": "^4.9.4" + }, + "peerDependencies": { + "ws": "*" } }, "node_modules/@babel/code-frame": { @@ -2513,6 +2516,27 @@ "typedarray-to-buffer": "^3.1.5" } }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -4509,6 +4533,13 @@ "typedarray-to-buffer": "^3.1.5" } }, + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "peer": true, + "requires": {} + }, "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index 1f67604..7fa8d71 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "description": "default socket.io in-memory adapter", + "peerDependencies": { + "ws": "*" + }, "devDependencies": { "@types/mocha": "^10.0.1", "@types/node": "^14.11.2", diff --git a/test/index.ts b/test/index.ts index c13a0db..e72a287 100644 --- a/test/index.ts +++ b/test/index.ts @@ -140,6 +140,48 @@ describe("socket.io-adapter", () => { expect(ids).to.eql(["s3"]); }); + it("should precompute the WebSocket frames when broadcasting", () => { + function socket(id) { + return [ + id, + { + id, + client: { + writeToEngine(payload, opts) { + expect(payload).to.eql(["123"]); + expect(opts.preEncoded).to.eql(true); + expect(opts.wsPreEncodedFrame.length).to.eql(2); + expect(opts.wsPreEncodedFrame[0]).to.eql(Buffer.from([129, 4])); + expect(opts.wsPreEncodedFrame[1]).to.eql( + Buffer.from([52, 49, 50, 51]) + ); + }, + }, + }, + ]; + } + const nsp = { + server: { + encoder: { + encode() { + return ["123"]; + }, + }, + }, + // @ts-ignore + sockets: new Map([socket("s1"), socket("s2"), socket("s3")]), + }; + const adapter = new Adapter(nsp); + adapter.addAll("s1", new Set()); + adapter.addAll("s2", new Set()); + adapter.addAll("s3", new Set()); + + adapter.broadcast([], { + rooms: new Set(), + except: new Set(), + }); + }); + describe("utility methods", () => { let adapter;