Skip to content

Commit 3833221

Browse files
committed
[feature]: zlib deflate concurrency limit
Ref: websockets#1202
1 parent 80445e7 commit 3833221

File tree

3 files changed

+59
-3
lines changed

3 files changed

+59
-3
lines changed

doc/ws.md

+5
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ provided then that is extension parameters:
7676
- `memLevel` {Number} The value of zlib's `memLevel` param (1-9, default 8).
7777
- `threshold` {Number} Payloads smaller than this will not be compressed.
7878
Defaults to 1024 bytes.
79+
- `concurrencyLimit` {Number} The number of concurrent calls to zlib.
80+
Calls above this limit will be queued. Default 10. You usually won't
81+
need to touch this option. See [concurrency-limit][this issue] for more
82+
details.
7983

8084
If a property is empty then either an offered configuration or a default value
8185
is used.
@@ -425,4 +429,5 @@ Forcibly close the connection.
425429

426430
The URL of the WebSocket server. Server clients don't have this attribute.
427431

432+
[concurrency-limit]: https://github.com/websockets/ws/issues/1202
428433
[permessage-deflate]: https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19

lib/PerMessageDeflate.js

+53-3
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,21 @@ const safeBuffer = require('safe-buffer');
44
const zlib = require('zlib');
55

66
const bufferUtil = require('./BufferUtil');
7+
const Limiter = require('async-limiter');
78

89
const Buffer = safeBuffer.Buffer;
910

1011
const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
1112
const EMPTY_BLOCK = Buffer.from([0x00]);
1213

14+
// We limit zlib concurrency, which prevents severe memory fragmentation
15+
// as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913
16+
// and https://github.com/websockets/ws/issues/1202
17+
//
18+
// Intentionally global; it's the global thread pool that's
19+
// an issue.
20+
let zlibLimiter;
21+
1322
/**
1423
* Per-message Deflate implementation.
1524
*/
@@ -25,6 +34,13 @@ class PerMessageDeflate {
2534
this._inflate = null;
2635

2736
this.params = null;
37+
38+
if (!zlibLimiter) {
39+
const concurrency = this._options.concurrencyLimit !== undefined
40+
? this._options.concurrencyLimit
41+
: 10;
42+
zlibLimiter = new Limiter({ concurrency });
43+
}
2844
}
2945

3046
static get extensionName () {
@@ -249,14 +265,48 @@ class PerMessageDeflate {
249265
}
250266

251267
/**
252-
* Decompress data.
268+
* Decompress data. Concurrency limited by async-limiter.
253269
*
254270
* @param {Buffer} data Compressed data
255271
* @param {Boolean} fin Specifies whether or not this is the last fragment
256272
* @param {Function} callback Callback
257273
* @public
258274
*/
259275
decompress (data, fin, callback) {
276+
zlibLimiter.push((done) => {
277+
this._decompress(data, fin, function (err, result) {
278+
done();
279+
callback(err, result);
280+
});
281+
});
282+
}
283+
284+
/**
285+
* Compress data. Concurrency limited by async-limiter.
286+
*
287+
* @param {Buffer} data Data to compress
288+
* @param {Boolean} fin Specifies whether or not this is the last fragment
289+
* @param {Function} callback Callback
290+
* @public
291+
*/
292+
compress (data, fin, callback) {
293+
zlibLimiter.push((done) => {
294+
this._compress(data, fin, function (err, result) {
295+
done();
296+
callback(err, result);
297+
});
298+
});
299+
}
300+
301+
/**
302+
* Decompress data.
303+
*
304+
* @param {Buffer} data Compressed data
305+
* @param {Boolean} fin Specifies whether or not this is the last fragment
306+
* @param {Function} callback Callback
307+
* @private
308+
*/
309+
_decompress (data, fin, callback) {
260310
const endpoint = this._isServer ? 'client' : 'server';
261311

262312
if (!this._inflate) {
@@ -322,9 +372,9 @@ class PerMessageDeflate {
322372
* @param {Buffer} data Data to compress
323373
* @param {Boolean} fin Specifies whether or not this is the last fragment
324374
* @param {Function} callback Callback
325-
* @public
375+
* @private
326376
*/
327-
compress (data, fin, callback) {
377+
_compress (data, fin, callback) {
328378
if (!data || data.length === 0) {
329379
process.nextTick(callback, null, EMPTY_BLOCK);
330380
return;

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"lint": "eslint ."
2727
},
2828
"dependencies": {
29+
"async-limiter": "^1.0.0",
2930
"safe-buffer": "~5.1.0",
3031
"ultron": "~1.1.0"
3132
},

0 commit comments

Comments
 (0)