@encharm/cws 4.10.0 → 4.11.2
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.
- package/CHANGELOG.md +10 -0
- package/CLAUDE.md +5 -1
- package/README.md +15 -0
- package/dist/bindings/cws_darwin_arm64_node115.node +0 -0
- package/dist/bindings/cws_darwin_arm64_node127.node +0 -0
- package/dist/bindings/cws_darwin_arm64_node137.node +0 -0
- package/dist/bindings/cws_darwin_arm64_node147.node +0 -0
- package/dist/bindings/cws_linux_arm64_node115.node +0 -0
- package/dist/bindings/cws_linux_arm64_node127.node +0 -0
- package/dist/bindings/cws_linux_arm64_node137.node +0 -0
- package/dist/bindings/cws_linux_arm64_node147.node +0 -0
- package/dist/bindings/cws_linux_x64_node115.node +0 -0
- package/dist/bindings/cws_linux_x64_node127.node +0 -0
- package/dist/bindings/cws_linux_x64_node137.node +0 -0
- package/dist/bindings/cws_linux_x64_node147.node +0 -0
- package/dist/bindings/cws_win32_x64_node115.node +0 -0
- package/dist/bindings/cws_win32_x64_node127.node +0 -0
- package/dist/bindings/cws_win32_x64_node137.node +0 -0
- package/dist/bindings/cws_win32_x64_node147.node +0 -0
- package/dist/client.d.ts +3 -1
- package/dist/client.js +10 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/prepared.d.ts +6 -0
- package/dist/prepared.js +17 -0
- package/package.json +1 -1
- package/src/Addon.h +41 -0
- package/src/MicroDeflate.h +97 -23
- package/src/WebSocket.cpp +149 -0
- package/src/WebSocket.h +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## Released 4.11.2
|
|
2
|
+
* microdeflate, output unchanged: match symbols write the bit accumulator unconditionally (their sizes vary, so the flush branch mispredicted), literals keep the predictable conditional flush; the 32 KB distance-code table is replaced by a computed code. Profiled with hardware counters on the production EPYC 9454P: 1.84 -> 1.72 µs/KB on the RPC capture (1.58 -> 1.48 on 24 KB messages), 1.13 -> 1.00 on Apple M; incompressible input unchanged. Tried and rejected with measurements: a packed 32 KB hash table (gcc emits partial-register masks on Zen), a single-branch candidate check (an unconditional random load costs more than the mispredicts it saves), `__restrict`, and smaller or larger tables.
|
|
3
|
+
|
|
4
|
+
## Released 4.11.1
|
|
5
|
+
* microdeflate speed-ups, output unchanged: matches extend 8 bytes per step, the bit writer stores 8 bytes at a time, length/distance codes go out with their extra bits in one write, and each hash slot carries an 8-bit tag so a stale candidate is rejected without touching the input. Measured on the RPC capture: 1.74 -> 1.20 µs/KB (1.45x), byte-identical output. Incompressible input (random bytes) now falls back to stored blocks: 3x faster and 0.999 instead of 0.948 ratio.
|
|
6
|
+
|
|
7
|
+
## Released 4.11.0
|
|
8
|
+
* Prepared messages for fan-out: `new PreparedMessage(bytes)` copies a payload into native memory once; `ws.send(prepared, { prefix })` sends `prefix + payload` as one frame without copying or compressing the payload per socket. Uncompressed, the payload goes out as a second gather buffer. Compressed (shared compressor mode), the prefix is emitted as a DEFLATE stored block ahead of the payload's deflate blocks, which are built once on the first compressed send and cached on the handle; standard inflaters accept the result unchanged. Sockets with context takeover and client sockets take the regular copying path. Measured with 300 subscribers and a 3 KB payload: 0.9 -> 0.2 µs of JS-thread time per socket, and no per-socket compression at all.
|
|
9
|
+
* `send()` accepts any `ArrayBufferView` (typed arrays are sent from their own offset, no `Buffer.from` copy needed).
|
|
10
|
+
|
|
1
11
|
## Released 4.10.0
|
|
2
12
|
* microdeflate: a built-in ~150-line raw-DEFLATE encoder (fixed Huffman, greedy LZ77, no stream state, no per-message hash reset) now compresses independent messages, i.e. the shared-compressor mode. Measured on a real BSON RPC stream: same ratio as zlib-ng level 1 (2.84 vs 2.85) at ~1.7x its speed; a compressed 2 KB message costs 4.6 µs of worker CPU instead of 6.0. Output is standard DEFLATE (round-trip tested against zlib's inflater). `CWS_MICRO_DEFLATE=0` falls back to zlib-ng; `zlibBackend` reports `+ microdeflate` when active. Dedicated windows (context takeover) and inflate stay on zlib-ng.
|
|
3
13
|
* Compression moved to the send worker: `send()` of a compressed message queues the raw payload and the worker deflates + frames it. Main-thread cost of a compressed 2 KB RPC message drops from ~6 µs to ~0.7 µs (Linux, per-thread measurement); wire output is byte-identical. Main-thread write paths (same-tick terminate, full worker queue, drain loop after a short write) deflate pending messages themselves first.
|
package/CLAUDE.md
CHANGED
|
@@ -25,7 +25,7 @@ Tests bind ports 3000 (ws) and 3001 (wss, certs in `tests/certs/`). The test fil
|
|
|
25
25
|
|
|
26
26
|
1. Download official Node header tarballs for one pinned version per supported major into `targets/` (`VER_115`=Node 20, `VER_127`=Node 22, `VER_137`=Node 24, `VER_147`=Node 26; the number is the Node ABI / `process.versions.modules`).
|
|
27
27
|
2. Compile `src/*.cpp` once per ABI with `g++`/`cl` directly, with `-I src/headers/$V` for the matching Node major.
|
|
28
|
-
3. Build the vendored zlib-ng (`deps/zlib-ng`, native `zng_` API) once per OS/arch: CMake into `deps/zlib-ng/build-<OS>-<arch>/` on macOS/Linux, `nmake -f win32\Makefile.msc` on Windows. The bindings are compiled with `-DCWS_ZLIB_NG` and link it statically. `src/MicroDeflate.h` is a self-contained fixed-Huffman DEFLATE encoder used for independent messages (shared mode) via `zlib::deflateIndependent
|
|
28
|
+
3. Build the vendored zlib-ng (`deps/zlib-ng`, native `zng_` API) once per OS/arch: CMake into `deps/zlib-ng/build-<OS>-<arch>/` on macOS/Linux, `nmake -f win32\Makefile.msc` on Windows. The bindings are compiled with `-DCWS_ZLIB_NG` and link it statically. `src/MicroDeflate.h` is a self-contained fixed-Huffman DEFLATE encoder used for independent messages (shared mode) via `zlib::deflateIndependent` (greedy LZ77 over a tagged 13-bit hash table, 8-byte match extension, computed distance codes, stored-block fallback when the input does not compress; output must stay byte-identical across speed work, and speed claims need the production EPYC, not only Apple silicon: gcc/Zen and clang/M-series disagreed on every layout change in 4.11.2, only the hybrid bit-writer flush won on both); Measured and rejected on the RPC capture, so do not retry without new data: dynamic Huffman trees per block (+9.4% ratio at 2.4x the time on EPYC), two candidates per hash slot (+2.5% at 1.5x), 3-byte minimum matches (worse ratio), packed 32 KB table with 16-bit tags (gcc partial-register stalls on Zen), single-branch candidate check (unconditional random load), `__restrict`, 12/14-bit tables. libdeflate level 1 reaches 3.28 vs our 2.86 at 2.6x the time; `src/Zlib.cpp` is the only file that includes a zlib header; without the define (node-gyp fallback) it uses Node's zlib.
|
|
29
29
|
4. Emit `dist/bindings/cws_<platform>_<arch>_node<ABI>.node`.
|
|
30
30
|
|
|
31
31
|
```sh
|
|
@@ -76,6 +76,10 @@ The client side never touches Node sockets: `native.connect(clientGroup, url, ws
|
|
|
76
76
|
|
|
77
77
|
`SendWorker` (`src/SendWorker.cpp`) starts one `std::thread` at addon load when corking is enabled. `Socket::uncork` moves up to 512 queued frames into a `Socket::SendOp` (ownership moves with them) and hands it over through `deps/readerwriterqueue` (blocking SPSC main→worker, plain SPSC worker→main plus a `uv_async`). The worker only calls `sendmsg`/`WSASend` and fills `result`/`error`; `Socket::sendComplete` on the main thread pops what was sent, requeues the rest at the head, resubmits if more queued, arms `UV_WRITABLE` for the classic drain loop on a short write, runs send callbacks, and calls `endCb` (= `STATE::onEnd`, set in `setState`) on a hard error. While an op is in flight `write()` appends to the queue and the drain loop stays out. `closeSocket` orphans an in-flight op (`socket = nullptr`, `closeFd = true`) and lets the completion close the fd, so a reused fd number can never receive the old socket's bytes. SSL sockets never use the worker. `CWS_SEND_THREAD=0` disables. Compressed sends: `WebSocket::send` queues the raw payload as a `compressPending` message (`enqueueCompressPending`); `performSend` deflates + frames it on the worker with the socket's window (`op->deflateWindow`) or the worker's own shared compressor, then builds the iovecs. Any main-thread write path calls `materializePending()` first (`WebSocket::materialize` via `materializeCb`, using the hub's compressor). A socket closed mid-flight hands its window to the op (`destroyWindow`, `workerOwnsWindow`) so `onEnd` does not free it under the worker.
|
|
78
78
|
|
|
79
|
+
### Prepared (shared) messages
|
|
80
|
+
|
|
81
|
+
`cWS::SharedPayload` (`src/WebSocket.h`) holds one payload for many recipients: the raw bytes plus, after the first compressed send, their deflate blocks (`Hub::deflate` with no window, i.e. the shared compressor; sync-flush tail stripped like every compressed message). `WebSocket::sendShared(prefix, payload, ...)` queues one frame as two `Queue::Message`s: an owned one with the frame header and the prefix, and a borrowed one (`ownsData = false`) pointing into the payload whose completion callback (`sharedSent`) drops the payload reference and then runs the user's callback. Compressed, the prefix becomes DEFLATE stored blocks (`00 LEN NLEN`, BFINAL=0) ahead of the cached blocks; that is only valid without context takeover, so a socket with a `slidingDeflateWindow`, the client role (masking) and an empty payload fall back to a contiguous `send()`. JS: `PreparedMessage` (`lib/prepared.ts`, native handle released through a `FinalizationRegistry`) and `WebSocket.send` routes a `PreparedMessage` (with the optional `prefix` option) to `native.sendShared`.
|
|
82
|
+
|
|
79
83
|
### Things that are easy to get wrong
|
|
80
84
|
|
|
81
85
|
- `dist/*.js` and `dist/bindings/*.node` are committed. Rebuild TS with `npm run build-ts` and commit the output; binaries must be rebuilt on each platform when C++ changes.
|
package/README.md
CHANGED
|
@@ -294,6 +294,21 @@ With permessage-deflate enabled, compression of outgoing messages also runs on t
|
|
|
294
294
|
|
|
295
295
|
Set `CWS_SEND_THREAD=0` to disable it (sends and compression then happen on the main thread at the end of the tick). The `sendThread` export reports `'active'` or the reason it is not. TLS sockets always send on the main thread.
|
|
296
296
|
|
|
297
|
+
### Prepared messages (fan-out)
|
|
298
|
+
|
|
299
|
+
When one payload goes to many sockets with only a small per-socket prefix in front of it (an RPC header, a subscription id), prepare it once:
|
|
300
|
+
|
|
301
|
+
```js
|
|
302
|
+
const { PreparedMessage } = require('@encharm/cws');
|
|
303
|
+
|
|
304
|
+
const prepared = new PreparedMessage(payloadBytes); // copied into native memory once
|
|
305
|
+
for (const ws of subscribers) {
|
|
306
|
+
ws.send(prepared, { prefix: headerFor(ws) }); // one frame: header + payload
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`send` accepts a `PreparedMessage` wherever it accepts a buffer, plus a `prefix` option (a per-socket header, string or bytes) spliced in front of the payload inside the same frame. `binary` defaults to `true` for prepared messages; `compress` defaults to the server's `threshold` applied to the total length. The payload is never copied or compressed per socket: without deflate it is a second gather buffer in the end-of-tick write; with the shared compressor it is deflated once, on the first compressed send, and the prefix is spliced in front as a DEFLATE stored block. Sockets negotiated with context takeover (`serverNoContextTakeover: false`) and client sockets fall back to the regular path. Handles are released when garbage collected; sends in flight keep their own reference.
|
|
311
|
+
|
|
297
312
|
### Secure WebSocket
|
|
298
313
|
You can use `wss://` with `cws` by providing `https` server to `cws` and setting `secureProtocol` on https options:
|
|
299
314
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { WebSocketServer } from './server';
|
|
3
3
|
import { SocketAddress, ServerConfigs } from './index';
|
|
4
|
+
import { PreparedMessage } from './prepared';
|
|
4
5
|
export declare class WebSocket {
|
|
5
6
|
url: string;
|
|
6
7
|
private options;
|
|
@@ -28,9 +29,10 @@ export declare class WebSocket {
|
|
|
28
29
|
on(event: 'error', listener: (err: Error) => void): void;
|
|
29
30
|
on(event: 'message', listener: (message: string | any) => void): void;
|
|
30
31
|
on(event: 'close', listener: (code?: number, reason?: string) => void): void;
|
|
31
|
-
send(message: string | Buffer, options?: {
|
|
32
|
+
send(message: string | Buffer | ArrayBufferView | PreparedMessage, options?: {
|
|
32
33
|
binary?: boolean;
|
|
33
34
|
compress?: boolean;
|
|
35
|
+
prefix?: string | Buffer | ArrayBufferView;
|
|
34
36
|
}, cb?: (err?: Error) => void): void;
|
|
35
37
|
ping(message?: string | Buffer): void;
|
|
36
38
|
close(code?: number, reason?: string): void;
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.WebSocket = void 0;
|
|
4
4
|
const server_1 = require("./server");
|
|
5
5
|
const shared_1 = require("./shared");
|
|
6
|
+
const prepared_1 = require("./prepared");
|
|
6
7
|
const clientGroup = shared_1.native.client.group.create(0, shared_1.DEFAULT_PAYLOAD_LIMIT);
|
|
7
8
|
function messageByteLength(message) {
|
|
8
9
|
if (typeof message === 'string') {
|
|
@@ -78,6 +79,7 @@ class WebSocket {
|
|
|
78
79
|
}
|
|
79
80
|
send(message, options, cb) {
|
|
80
81
|
if (this.external) {
|
|
82
|
+
const prepared = message instanceof prepared_1.PreparedMessage;
|
|
81
83
|
let opCode = typeof message === 'string' ? shared_1.OPCODE_TEXT : shared_1.OPCODE_BINARY;
|
|
82
84
|
if (options && options.binary === false) {
|
|
83
85
|
opCode = shared_1.OPCODE_TEXT;
|
|
@@ -90,9 +92,15 @@ class WebSocket {
|
|
|
90
92
|
compress = !!options.compress;
|
|
91
93
|
}
|
|
92
94
|
else if (this.compressThreshold !== undefined) {
|
|
93
|
-
compress = messageByteLength(message) >= this.compressThreshold;
|
|
95
|
+
compress = messageByteLength(message) + (options && options.prefix ? messageByteLength(options.prefix) : 0) >= this.compressThreshold;
|
|
96
|
+
}
|
|
97
|
+
const callback = cb ? () => process.nextTick(cb) : null;
|
|
98
|
+
if (prepared) {
|
|
99
|
+
this.nativeApi.sendShared(this.external, options && options.prefix, message.external, opCode, callback, compress);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.nativeApi.send(this.external, message, opCode, callback, compress);
|
|
94
103
|
}
|
|
95
|
-
this.nativeApi.send(this.external, message, opCode, cb ? () => process.nextTick(cb) : null, compress);
|
|
96
104
|
}
|
|
97
105
|
else if (cb) {
|
|
98
106
|
cb(new Error('Socket not connected'));
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export declare type ServerConfigs = {
|
|
|
30
30
|
verifyClient?: (info: ConnectionInfo, next: VerifyClientNext) => void;
|
|
31
31
|
};
|
|
32
32
|
export { WebSocket } from './client';
|
|
33
|
+
export { PreparedMessage } from './prepared';
|
|
33
34
|
export { WebSocketServer } from './server';
|
|
34
35
|
export declare const secureProtocol: string;
|
|
35
36
|
export { zlibBackend, sendThread } from './shared';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.secureProtocol = void 0;
|
|
4
4
|
var client_1 = require("./client");
|
|
5
5
|
Object.defineProperty(exports, "WebSocket", { enumerable: true, get: function () { return client_1.WebSocket; } });
|
|
6
|
+
var prepared_1 = require("./prepared");
|
|
7
|
+
Object.defineProperty(exports, "PreparedMessage", { enumerable: true, get: function () { return prepared_1.PreparedMessage; } });
|
|
6
8
|
var server_1 = require("./server");
|
|
7
9
|
Object.defineProperty(exports, "WebSocketServer", { enumerable: true, get: function () { return server_1.WebSocketServer; } });
|
|
8
10
|
exports.secureProtocol = 'TLSv1_2_method';
|
package/dist/prepared.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PreparedMessage = void 0;
|
|
4
|
+
const shared_1 = require("./shared");
|
|
5
|
+
const preparedRegistry = typeof global.FinalizationRegistry === 'function'
|
|
6
|
+
? new global.FinalizationRegistry((external) => shared_1.native.server.releaseShared(external))
|
|
7
|
+
: undefined;
|
|
8
|
+
class PreparedMessage {
|
|
9
|
+
constructor(data) {
|
|
10
|
+
this.byteLength = data.byteLength;
|
|
11
|
+
this.external = shared_1.native.server.prepareShared(data);
|
|
12
|
+
if (preparedRegistry) {
|
|
13
|
+
preparedRegistry.register(this, this.external);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.PreparedMessage = PreparedMessage;
|
package/package.json
CHANGED
package/src/Addon.h
CHANGED
|
@@ -644,6 +644,44 @@ void sendPrepared(const FunctionCallbackInfo<Value> &args) {
|
|
|
644
644
|
->Value());
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
+
template <bool isServer>
|
|
648
|
+
void prepareShared(const FunctionCallbackInfo<Value> &args) {
|
|
649
|
+
NativeString nativeString(args.GetIsolate(), args[0]);
|
|
650
|
+
args.GetReturnValue().Set(External::New(
|
|
651
|
+
args.GetIsolate(),
|
|
652
|
+
cWS::WebSocket<isServer>::prepareShared(nativeString.getData(),
|
|
653
|
+
nativeString.getLength())));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
template <bool isServer>
|
|
657
|
+
void releaseShared(const FunctionCallbackInfo<Value> &args) {
|
|
658
|
+
cWS::SharedPayload::unref(
|
|
659
|
+
(cWS::SharedPayload *)args[0].As<External>()->Value());
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// (socket, prefix, payload, opCode, callback, compress)
|
|
663
|
+
template <bool isServer>
|
|
664
|
+
void sendShared(const FunctionCallbackInfo<Value> &args) {
|
|
665
|
+
NativeString prefix(args.GetIsolate(), args[1]);
|
|
666
|
+
cWS::SharedPayload *payload =
|
|
667
|
+
(cWS::SharedPayload *)args[2].As<External>()->Value();
|
|
668
|
+
cWS::OpCode opCode = (cWS::OpCode)args[3].As<Integer>()->Value();
|
|
669
|
+
|
|
670
|
+
SendCallbackData *sc = nullptr;
|
|
671
|
+
void (*callback)(cWS::WebSocket<isServer> *, void *, bool, void *) = nullptr;
|
|
672
|
+
if (args[4]->IsFunction()) {
|
|
673
|
+
callback = sendCallback;
|
|
674
|
+
sc = new SendCallbackData;
|
|
675
|
+
sc->jsCallback.Reset(args.GetIsolate(), Local<Function>::Cast(args[4]));
|
|
676
|
+
sc->isolate = args.GetIsolate();
|
|
677
|
+
}
|
|
678
|
+
bool compress = args[5].As<Boolean>()->Value();
|
|
679
|
+
|
|
680
|
+
unwrapSocket<isServer>(args[0].As<External>())
|
|
681
|
+
->sendShared(prefix.getData(), prefix.getLength(), payload, opCode,
|
|
682
|
+
compress, callback, sc);
|
|
683
|
+
}
|
|
684
|
+
|
|
647
685
|
template <bool isServer>
|
|
648
686
|
void finalizeMessage(const FunctionCallbackInfo<Value> &args) {
|
|
649
687
|
cWS::WebSocket<isServer>::finalizeMessage(
|
|
@@ -723,6 +761,9 @@ struct Namespace {
|
|
|
723
761
|
NODE_SET_METHOD(object, "prepareMessage", prepareMessage<isServer>);
|
|
724
762
|
NODE_SET_METHOD(object, "sendPrepared", sendPrepared<isServer>);
|
|
725
763
|
NODE_SET_METHOD(object, "finalizeMessage", finalizeMessage<isServer>);
|
|
764
|
+
NODE_SET_METHOD(object, "prepareShared", prepareShared<isServer>);
|
|
765
|
+
NODE_SET_METHOD(object, "sendShared", sendShared<isServer>);
|
|
766
|
+
NODE_SET_METHOD(object, "releaseShared", releaseShared<isServer>);
|
|
726
767
|
|
|
727
768
|
Local<Object> group = Object::New(isolate);
|
|
728
769
|
NODE_SET_METHOD(group, "onConnection", onConnection<isServer>);
|
package/src/MicroDeflate.h
CHANGED
|
@@ -15,11 +15,15 @@
|
|
|
15
15
|
#include <cstdint>
|
|
16
16
|
#include <cstring>
|
|
17
17
|
#include <cstddef>
|
|
18
|
+
#ifdef _MSC_VER
|
|
19
|
+
#include <intrin.h>
|
|
20
|
+
#endif
|
|
18
21
|
|
|
19
22
|
namespace cWS {
|
|
20
23
|
namespace microdeflate {
|
|
21
24
|
|
|
22
|
-
// Worst case output for `length` input bytes (all literals at 9 bits + block overhead)
|
|
25
|
+
// Worst case output for `length` input bytes (all literals at 9 bits + block overhead),
|
|
26
|
+
// plus slack for the 8-byte stores of the bit writer.
|
|
23
27
|
inline size_t bound(size_t length) {
|
|
24
28
|
return length + length / 8 + 32;
|
|
25
29
|
}
|
|
@@ -27,6 +31,9 @@ inline size_t bound(size_t length) {
|
|
|
27
31
|
class Encoder {
|
|
28
32
|
static const int HASH_BITS = 13;
|
|
29
33
|
uint32_t table[1 << HASH_BITS];
|
|
34
|
+
// 8 more hash bits per slot: a candidate whose tag differs cannot match, and rejecting it
|
|
35
|
+
// here saves the dependent random load into the input that dominated the literal path.
|
|
36
|
+
uint8_t tags[1 << HASH_BITS];
|
|
30
37
|
|
|
31
38
|
static const uint16_t *lenBase() { static const uint16_t t[29] = {3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258}; return t; }
|
|
32
39
|
static const uint8_t *lenExtra() { static const uint8_t t[29] = {0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0}; return t; }
|
|
@@ -36,7 +43,6 @@ class Encoder {
|
|
|
36
43
|
struct Tables {
|
|
37
44
|
uint16_t litCode[288]; uint8_t litBits[288];
|
|
38
45
|
uint8_t lenCode[259];
|
|
39
|
-
uint8_t distCode[32769];
|
|
40
46
|
uint8_t distCodeRev[30];
|
|
41
47
|
static uint32_t rev(uint32_t v, int n) { uint32_t r = 0; for (int i = 0; i < n; i++) { r = (r << 1) | (v & 1); v >>= 1; } return r; }
|
|
42
48
|
Tables() {
|
|
@@ -49,21 +55,85 @@ class Encoder {
|
|
|
49
55
|
litCode[i] = (uint16_t) rev(code, bits); litBits[i] = (uint8_t) bits;
|
|
50
56
|
}
|
|
51
57
|
for (int c = 0; c < 29; c++) for (int l = lenBase()[c]; l < (c < 28 ? lenBase()[c + 1] : 259); l++) lenCode[l] = (uint8_t) c;
|
|
52
|
-
for (int c = 0; c < 30; c++)
|
|
58
|
+
for (int c = 0; c < 30; c++) distCodeRev[c] = (uint8_t) rev(c, 5);
|
|
53
59
|
}
|
|
54
60
|
};
|
|
55
61
|
static const Tables &tables() { static const Tables t; return t; }
|
|
56
62
|
|
|
63
|
+
// 64-bit accumulator flushed 8 bytes at a time with one unaligned store (the output
|
|
64
|
+
// buffer has slack for the over-write, see bound()). Literals (8 or 9 bits) drain the
|
|
65
|
+
// accumulator conditionally: their flush pattern repeats every few symbols and predicts
|
|
66
|
+
// well, and an unconditional store per literal made incompressible input store-bound on
|
|
67
|
+
// Zen 4 (+58%). Match symbols vary in size, so their flush branch mispredicted; they
|
|
68
|
+
// store unconditionally instead (-7% on the RPC capture on Zen 4, -10% on Apple M).
|
|
57
69
|
struct BitWriter {
|
|
58
70
|
uint8_t *out; size_t pos = 0; uint64_t acc = 0; int n = 0;
|
|
59
|
-
inline void put(uint32_t v, int bits) {
|
|
60
|
-
|
|
71
|
+
inline void put(uint32_t v, int bits) {
|
|
72
|
+
acc |= (uint64_t) v << n; n += bits;
|
|
73
|
+
memcpy(out + pos, &acc, 8); pos += n >> 3; acc >>= n & ~7; n &= 7;
|
|
74
|
+
}
|
|
75
|
+
inline void putLit(uint32_t v, int bits) {
|
|
76
|
+
acc |= (uint64_t) v << n; n += bits;
|
|
77
|
+
if (n >= 32) { memcpy(out + pos, &acc, 8); pos += n >> 3; acc >>= n & ~7; n &= 7; }
|
|
78
|
+
}
|
|
79
|
+
inline void flushByte() { while (n > 0) { out[pos++] = (uint8_t) acc; acc >>= 8; n -= 8; } acc = 0; n = 0; }
|
|
61
80
|
};
|
|
62
81
|
|
|
63
|
-
static inline uint32_t
|
|
82
|
+
static inline uint32_t load32(const uint8_t *p) { uint32_t v; memcpy(&v, p, 4); return v; }
|
|
83
|
+
static inline uint64_t load64(const uint8_t *p) { uint64_t v; memcpy(&v, p, 8); return v; }
|
|
84
|
+
static inline uint32_t hash32(const uint8_t *p) { return load32(p) * 2654435761u; }
|
|
85
|
+
static inline uint32_t slot(uint32_t h) { return h >> (32 - HASH_BITS); }
|
|
86
|
+
static inline uint8_t tag(uint32_t h) { return (uint8_t) (h >> (32 - HASH_BITS - 8)); }
|
|
87
|
+
// Distance code from the highest set bit of dist-1: codes 0-3 are exact, then two codes
|
|
88
|
+
// per power of two, the lower one chosen by the bit below the msb. Replaces a 32 KB table.
|
|
89
|
+
static inline int distCode(uint32_t dist) {
|
|
90
|
+
uint32_t d = dist - 1;
|
|
91
|
+
if (d < 4) return (int) d;
|
|
92
|
+
#ifdef _MSC_VER
|
|
93
|
+
unsigned long msb; _BitScanReverse(&msb, d);
|
|
94
|
+
#else
|
|
95
|
+
int msb = 31 - __builtin_clz(d);
|
|
96
|
+
#endif
|
|
97
|
+
return ((int) msb << 1) | (int) ((d >> (msb - 1)) & 1);
|
|
98
|
+
}
|
|
99
|
+
static inline int ctz64(uint64_t v) {
|
|
100
|
+
#ifdef _MSC_VER
|
|
101
|
+
unsigned long r; _BitScanForward64(&r, v); return (int) r;
|
|
102
|
+
#else
|
|
103
|
+
return __builtin_ctzll(v);
|
|
104
|
+
#endif
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Length of the common prefix of a and b, at most `max`: 8 bytes per step while
|
|
108
|
+
// both sides have 8 bytes left (little-endian: the first differing byte is the
|
|
109
|
+
// lowest set byte of the xor), then byte by byte.
|
|
110
|
+
static inline size_t matchLength(const uint8_t *a, const uint8_t *b, size_t max) {
|
|
111
|
+
size_t m = 0;
|
|
112
|
+
while (m + 8 <= max) {
|
|
113
|
+
uint64_t x = load64(a + m) ^ load64(b + m);
|
|
114
|
+
if (x) return m + (ctz64(x) >> 3);
|
|
115
|
+
m += 8;
|
|
116
|
+
}
|
|
117
|
+
while (m < max && a[m] == b[m]) m++;
|
|
118
|
+
return m;
|
|
119
|
+
}
|
|
64
120
|
|
|
65
121
|
public:
|
|
66
|
-
Encoder() { memset(table, 0xff, sizeof(table)); tables(); }
|
|
122
|
+
Encoder() { memset(table, 0xff, sizeof(table)); memset(tags, 0, sizeof(tags)); tables(); }
|
|
123
|
+
|
|
124
|
+
// Stored blocks: what an incompressible message costs, 5 bytes per 65535 plus the tail.
|
|
125
|
+
static size_t storeBlocks(const uint8_t *in, size_t length, uint8_t *out) {
|
|
126
|
+
size_t pos = 0;
|
|
127
|
+
for (size_t i = 0; i < length; ) {
|
|
128
|
+
size_t n = length - i > 65535 ? 65535 : length - i;
|
|
129
|
+
out[pos++] = 0; // BFINAL=0, BTYPE=00
|
|
130
|
+
out[pos++] = (uint8_t) n; out[pos++] = (uint8_t) (n >> 8);
|
|
131
|
+
out[pos++] = (uint8_t) ~n; out[pos++] = (uint8_t) (~n >> 8);
|
|
132
|
+
memcpy(out + pos, in + i, n); pos += n; i += n;
|
|
133
|
+
}
|
|
134
|
+
out[pos++] = 0; // empty stored block header, byte aligned; the 00 00 ff ff tail is implied
|
|
135
|
+
return pos;
|
|
136
|
+
}
|
|
67
137
|
|
|
68
138
|
// `out` must have room for bound(length) bytes. Returns the compressed length
|
|
69
139
|
// (without the 4-byte sync-flush tail).
|
|
@@ -74,30 +144,34 @@ public:
|
|
|
74
144
|
size_t i = 0;
|
|
75
145
|
const size_t limit = length >= 4 ? length - 4 : 0;
|
|
76
146
|
while (i < limit) {
|
|
77
|
-
uint32_t h =
|
|
78
|
-
|
|
79
|
-
table[
|
|
80
|
-
|
|
147
|
+
uint32_t h = hash32(in + i), s = slot(h);
|
|
148
|
+
uint8_t g = tag(h);
|
|
149
|
+
uint32_t cand = table[s];
|
|
150
|
+
bool tagged = tags[s] == g;
|
|
151
|
+
table[s] = (uint32_t) i;
|
|
152
|
+
tags[s] = g;
|
|
153
|
+
if (tagged && cand < i && i - cand <= 32768 && load32(in + cand) == load32(in + i)) {
|
|
81
154
|
size_t maxLen = length - i; if (maxLen > 258) maxLen = 258;
|
|
82
|
-
size_t m = 4;
|
|
83
|
-
while (m < maxLen && in[cand + m] == in[i + m]) m++;
|
|
155
|
+
size_t m = 4 + matchLength(in + cand + 4, in + i + 4, maxLen - 4);
|
|
84
156
|
uint32_t dist = (uint32_t) (i - cand);
|
|
85
157
|
int lc = t.lenCode[m];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
int dc =
|
|
89
|
-
w.put(t.distCodeRev[dc], 5);
|
|
90
|
-
if (
|
|
91
|
-
if (i + 1 < limit) table[hash4(in + i + 1)] = (uint32_t) (i + 1);
|
|
158
|
+
// length code + its extra bits in one put (at most 8 + 5 bits), same for distance (5 + 13)
|
|
159
|
+
w.put(t.litCode[257 + lc] | ((uint32_t) (m - lenBase()[lc]) << t.litBits[257 + lc]), t.litBits[257 + lc] + lenExtra()[lc]);
|
|
160
|
+
int dc = distCode(dist);
|
|
161
|
+
w.put(t.distCodeRev[dc] | ((dist - distBase()[dc]) << 5), 5 + distExtra()[dc]);
|
|
162
|
+
if (i + 1 < limit) { uint32_t h1 = hash32(in + i + 1); table[slot(h1)] = (uint32_t) (i + 1); tags[slot(h1)] = tag(h1); }
|
|
92
163
|
i += m;
|
|
93
164
|
} else {
|
|
94
|
-
w.
|
|
165
|
+
w.putLit(t.litCode[in[i]], t.litBits[in[i]]);
|
|
95
166
|
i++;
|
|
96
167
|
}
|
|
97
168
|
}
|
|
98
|
-
for (; i < length; i++) w.
|
|
99
|
-
w.
|
|
100
|
-
w.
|
|
169
|
+
for (; i < length; i++) w.putLit(t.litCode[in[i]], t.litBits[in[i]]);
|
|
170
|
+
w.putLit(t.litCode[256], t.litBits[256]); // end of block
|
|
171
|
+
w.putLit(0, 1); w.putLit(0, 2); w.flushByte(); // empty stored block: BFINAL=0, BTYPE=00, then byte-align
|
|
172
|
+
if (w.pos > length + 5 * (length / 65535 + 1) + 1) {
|
|
173
|
+
return storeBlocks(in, length, out); // incompressible: stored blocks are smaller
|
|
174
|
+
}
|
|
101
175
|
out[w.pos++] = 0; out[w.pos++] = 0; out[w.pos++] = 0xff; out[w.pos++] = 0xff;
|
|
102
176
|
return w.pos - 4;
|
|
103
177
|
}
|
package/src/WebSocket.cpp
CHANGED
|
@@ -82,6 +82,155 @@ void WebSocket<isServer>::materialize(cS::Socket *s, cS::Socket::Queue::Message
|
|
|
82
82
|
cS::Socket::materializeOnMain(s, m, stream, !webSocket->slidingDeflateWindow, hub->zlibBuffer, Hub::LARGE_BUFFER_SIZE, hub->dynamicZlibBuffer);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
template <bool isServer>
|
|
86
|
+
SharedPayload *WebSocket<isServer>::prepareShared(const char *data, size_t length) {
|
|
87
|
+
SharedPayload *payload = new SharedPayload;
|
|
88
|
+
payload->raw.assign(data, length);
|
|
89
|
+
return payload;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
namespace {
|
|
93
|
+
struct SharedCallback {
|
|
94
|
+
void (*callback)(void *socket, void *data, bool cancelled, void *reserved);
|
|
95
|
+
void *data;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Completion of the shared part of a sendShared() frame: drops the payload reference,
|
|
99
|
+
// then runs the caller's callback. `socket` is null when the send was orphaned by a close.
|
|
100
|
+
void sharedSent(void *socket, void *data, bool cancelled, void *reserved) {
|
|
101
|
+
SharedPayload::unref((SharedPayload *) data);
|
|
102
|
+
if (reserved) {
|
|
103
|
+
SharedCallback *c = (SharedCallback *) reserved;
|
|
104
|
+
c->callback(socket, c->data, cancelled, nullptr);
|
|
105
|
+
delete c;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* Sends prefix + payload as one frame. The frame is queued as two messages: an owned
|
|
112
|
+
* one with the header and the prefix, and a borrowed one pointing into the payload, so
|
|
113
|
+
* nothing is copied or compressed per recipient.
|
|
114
|
+
*
|
|
115
|
+
* Compressed: the prefix goes out as DEFLATE stored blocks in front of the payload's
|
|
116
|
+
* pre-built blocks. That is valid because those blocks were produced from the payload
|
|
117
|
+
* alone, so no match reaches back before their start. It needs a socket without context
|
|
118
|
+
* takeover (no sliding window): with takeover the per-socket window would not contain
|
|
119
|
+
* these bytes, so that case, like the client role (masking rewrites every byte), takes
|
|
120
|
+
* the regular copying path.
|
|
121
|
+
*/
|
|
122
|
+
template <bool isServer>
|
|
123
|
+
void WebSocket<isServer>::sendShared(const char *prefix, size_t prefixLength, SharedPayload *payload, OpCode opCode, bool compress,
|
|
124
|
+
void(*callback)(WebSocket<isServer> *webSocket, void *data, bool cancelled, void *reserved), void *callbackData) {
|
|
125
|
+
bool deflate = compress && compressionStatus == WebSocket<isServer>::CompressionStatus::ENABLED && opCode < 3;
|
|
126
|
+
if (!isServer || (deflate && slidingDeflateWindow) || payload->raw.empty()) {
|
|
127
|
+
std::string whole;
|
|
128
|
+
whole.reserve(prefixLength + payload->raw.size());
|
|
129
|
+
whole.append(prefix, prefixLength).append(payload->raw);
|
|
130
|
+
send(whole.data(), whole.size(), opCode, callback, callbackData, compress);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (isClosed()) {
|
|
134
|
+
if (callback) {
|
|
135
|
+
callback(this, callbackData, true, nullptr);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const char *body;
|
|
141
|
+
size_t bodyLength;
|
|
142
|
+
if (deflate) {
|
|
143
|
+
if (!payload->deflatedReady) {
|
|
144
|
+
size_t length = payload->raw.size();
|
|
145
|
+
char *out = Group<isServer>::from(this)->hub->deflate((char *) payload->raw.data(), length, nullptr);
|
|
146
|
+
payload->deflated.assign(out, length);
|
|
147
|
+
payload->deflatedReady = true;
|
|
148
|
+
}
|
|
149
|
+
body = payload->deflated.data();
|
|
150
|
+
bodyLength = payload->deflated.size();
|
|
151
|
+
} else {
|
|
152
|
+
body = payload->raw.data();
|
|
153
|
+
bodyLength = payload->raw.size();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// a stored block carries at most 65535 bytes: 1 byte header, LEN, NLEN
|
|
157
|
+
const size_t STORED_MAX = 65535;
|
|
158
|
+
size_t storedBlocks = deflate ? (prefixLength + STORED_MAX - 1) / STORED_MAX : 0;
|
|
159
|
+
size_t prefixPart = prefixLength + storedBlocks * 5;
|
|
160
|
+
const size_t MAX_HEADER = 14;
|
|
161
|
+
|
|
162
|
+
Queue::Message *first = allocMessage(MAX_HEADER + prefixPart);
|
|
163
|
+
char *dst = (char *) first->data;
|
|
164
|
+
char *p = dst + WebSocketProtocol<isServer, WebSocket<isServer>>::formatMessage(dst, dst, 0, opCode, prefixPart + bodyLength, deflate);
|
|
165
|
+
if (deflate) {
|
|
166
|
+
const char *src = prefix;
|
|
167
|
+
for (size_t remaining = prefixLength; remaining; ) {
|
|
168
|
+
size_t n = remaining > STORED_MAX ? STORED_MAX : remaining;
|
|
169
|
+
*p++ = 0; // BFINAL=0, BTYPE=00, already byte aligned
|
|
170
|
+
*p++ = (char) (n & 0xff);
|
|
171
|
+
*p++ = (char) (n >> 8);
|
|
172
|
+
*p++ = (char) (~n & 0xff);
|
|
173
|
+
*p++ = (char) ((~n >> 8) & 0xff);
|
|
174
|
+
memcpy(p, src, n);
|
|
175
|
+
p += n;
|
|
176
|
+
src += n;
|
|
177
|
+
remaining -= n;
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
memcpy(p, prefix, prefixLength);
|
|
181
|
+
p += prefixLength;
|
|
182
|
+
}
|
|
183
|
+
first->length = p - dst;
|
|
184
|
+
|
|
185
|
+
int memoryIndex = nodeData->getMemoryBlockIndex(sizeof(Queue::Message));
|
|
186
|
+
Queue::Message *second = (Queue::Message *) nodeData->getSmallMemoryBlock(memoryIndex);
|
|
187
|
+
second->data = body;
|
|
188
|
+
second->length = bodyLength;
|
|
189
|
+
second->nextMessage = nullptr;
|
|
190
|
+
second->poolIndex = memoryIndex;
|
|
191
|
+
second->ownsData = false;
|
|
192
|
+
second->compressPending = false;
|
|
193
|
+
second->opCode = 0;
|
|
194
|
+
second->callback = sharedSent;
|
|
195
|
+
second->callbackData = payload;
|
|
196
|
+
second->reserved = callback ? new SharedCallback{(void(*)(void *, void *, bool, void *)) callback, callbackData} : nullptr;
|
|
197
|
+
payload->references++;
|
|
198
|
+
|
|
199
|
+
if (corkActive()) {
|
|
200
|
+
enqueue(first);
|
|
201
|
+
enqueue(second);
|
|
202
|
+
if (!corkPending) {
|
|
203
|
+
corkPending = true;
|
|
204
|
+
nodeData->corkState->pending.push_back(this);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// no write corking (CWS_CORK=0, TLS): write now, queue what does not fit, keep order
|
|
210
|
+
bool wasTransferred;
|
|
211
|
+
if (!write(first, wasTransferred)) {
|
|
212
|
+
freeMessage(first);
|
|
213
|
+
void *reserved = second->reserved;
|
|
214
|
+
nodeData->freeSmallMemoryBlock((char *) second, memoryIndex);
|
|
215
|
+
sharedSent(this, payload, true, reserved);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!wasTransferred) {
|
|
219
|
+
freeMessage(first);
|
|
220
|
+
}
|
|
221
|
+
if (!write(second, wasTransferred)) {
|
|
222
|
+
void *reserved = second->reserved;
|
|
223
|
+
nodeData->freeSmallMemoryBlock((char *) second, memoryIndex);
|
|
224
|
+
sharedSent(this, payload, true, reserved);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (!wasTransferred) {
|
|
228
|
+
void *reserved = second->reserved;
|
|
229
|
+
nodeData->freeSmallMemoryBlock((char *) second, memoryIndex);
|
|
230
|
+
sharedSent(this, payload, false, reserved);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
85
234
|
/*
|
|
86
235
|
* Prepares a single message for use with sendPrepared.
|
|
87
236
|
*
|
package/src/WebSocket.h
CHANGED
|
@@ -12,6 +12,23 @@ struct Group;
|
|
|
12
12
|
template <bool isServer>
|
|
13
13
|
struct HttpSocket;
|
|
14
14
|
|
|
15
|
+
// A payload prepared once and sent to many sockets with sendShared() (fan-out). Holds the
|
|
16
|
+
// raw bytes and, after the first compressed send, their DEFLATE blocks: the same bytes a
|
|
17
|
+
// regular compressed send would carry (sync-flush tail stripped). Reference counted: the
|
|
18
|
+
// creator holds one reference, every queued message holds one until it completes.
|
|
19
|
+
struct SharedPayload {
|
|
20
|
+
std::string raw;
|
|
21
|
+
std::string deflated;
|
|
22
|
+
bool deflatedReady = false;
|
|
23
|
+
int references = 1;
|
|
24
|
+
|
|
25
|
+
static void unref(SharedPayload *payload) {
|
|
26
|
+
if (!--payload->references) {
|
|
27
|
+
delete payload;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
15
32
|
template <const bool isServer>
|
|
16
33
|
struct WIN32_EXPORT WebSocket : cS::Socket, WebSocketState<isServer> {
|
|
17
34
|
protected:
|
|
@@ -74,6 +91,10 @@ public:
|
|
|
74
91
|
void ping(const char *message) {send(message, OpCode::PING);}
|
|
75
92
|
void send(const char *message, OpCode opCode = OpCode::TEXT) {send(message, strlen(message), opCode);}
|
|
76
93
|
void send(const char *message, size_t length, OpCode opCode, void(*callback)(WebSocket<isServer> *webSocket, void *data, bool cancelled, void *reserved) = nullptr, void *callbackData = nullptr, bool compress = false);
|
|
94
|
+
static SharedPayload *prepareShared(const char *data, size_t length);
|
|
95
|
+
// One frame carrying prefix + payload without copying or re-compressing the payload. Not thread safe.
|
|
96
|
+
void sendShared(const char *prefix, size_t prefixLength, SharedPayload *payload, OpCode opCode, bool compress,
|
|
97
|
+
void(*callback)(WebSocket<isServer> *webSocket, void *data, bool cancelled, void *reserved) = nullptr, void *callbackData = nullptr);
|
|
77
98
|
static PreparedMessage *prepareMessage(char *data, size_t length, OpCode opCode, bool compressed, void(*callback)(WebSocket<isServer> *webSocket, void *data, bool cancelled, void *reserved) = nullptr);
|
|
78
99
|
static PreparedMessage *prepareMessageBatch(std::vector<std::string> &messages, std::vector<int> &excludedMessages,
|
|
79
100
|
OpCode opCode, bool compressed, void(*callback)(WebSocket<isServer> *webSocket, void *data, bool cancelled, void *reserved) = nullptr);
|