@encharm/cws 4.11.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 +6 -0
- package/CLAUDE.md +1 -1
- 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/package.json +1 -1
- package/src/MicroDeflate.h +97 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
## Released 4.11.0
|
|
2
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.
|
|
3
9
|
* `send()` accepts any `ArrayBufferView` (typed arrays are sent from their own offset, no `Buffer.from` copy needed).
|
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
|
|
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/package.json
CHANGED
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
|
}
|