Repository: websockets/ws
Pull request #1199 (2017-09-05 23:29:59)

feat(perMessageDeflate): Allow 'level' option
This corresponds to the zlib level as described
in https://nodejs.org/api/zlib.html#zlib_class_zlib_deflateraw

This could be used, along with `threshold` to tune CPU usage and size. The default level in `zlib` is 6, which could be too high, but some users may find that 1 is worth the CPU savings for a quick pass.

For example, on some JSON sample data:

```
Running zlib.deflateRawSync for 1000 iterations.
Uncompressed Data Size: 137063 bytes

-----

Level 0: 550.879ms
Size: 137083 bytes
Level 1: 836.332ms
Size: 11379 bytes
Level 2: 825.474ms
Size: 10717 bytes
Level 3: 847.320ms
Size: 10397 bytes
Level 4: 1187.744ms
Size: 8388 bytes
Level 5: 1286.184ms
Size: 7761 bytes
Level 6: 1489.151ms
Size: 7372 bytes
Level 7: 1657.936ms
Size: 7301 bytes
Level 8: 1828.456ms
Size: 7187 bytes
Level 9: 1896.402ms
Size: 7181 bytes
```

lpinca: @STRML can you please add a line to https://github.com/websockets/ws/blob/master/doc/ws.md for this new option?
STRML: Sure, added.

Patch:
diff --git a/doc/ws.md b/doc/ws.md
index 9e5e6f41..c80ca5a1 100644
--- a/doc/ws.md
+++ b/doc/ws.md
@@ -72,7 +72,8 @@ provided then that is extension parameters:
 - `serverMaxWindowBits` {Number} The value of windowBits.
 - `clientMaxWindowBits` {Number} The value of max windowBits to be requested
   to clients.
-- `memLevel` {Number} The value of memLevel.
+- `level` {Number} The value of zlib's `level` param (0-9, default 8).
+- `memLevel` {Number} The value of zlib's `memLevel` param (1-9, default 8).
 - `threshold` {Number} Payloads smaller than this will not be compressed.
   Defaults to 1024 bytes.
 
diff --git a/lib/PerMessageDeflate.js b/lib/PerMessageDeflate.js
index c2c61375..7044b31f 100644
--- a/lib/PerMessageDeflate.js
+++ b/lib/PerMessageDeflate.js
@@ -340,6 +340,7 @@ class PerMessageDeflate {
 
       this._deflate = zlib.createDeflateRaw({
         memLevel: this._options.memLevel,
+        level: this._options.level,
         flush: zlib.Z_SYNC_FLUSH,
         windowBits
       });


Test patch:
diff --git a/test/PerMessageDeflate.test.js b/test/PerMessageDeflate.test.js
index dbaf67e4..b66ddd08 100644
--- a/test/PerMessageDeflate.test.js
+++ b/test/PerMessageDeflate.test.js
@@ -257,28 +257,77 @@ describe('PerMessageDeflate', function () {
     it('should compress/decompress data with parameters', function (done) {
       const perMessageDeflate = new PerMessageDeflate({
         threshold: 0,
-        memLevel: 5
+        memLevel: 5,
+        level: 9
       });
       const extensions = Extensions.parse(
         'permessage-deflate; server_no_context_takeover; ' +
         'client_no_context_takeover; server_max_window_bits=10; ' +
         'client_max_window_bits=11'
       );
+      const srcData = 'Some compressible data, it\'s compressible.';
 
       perMessageDeflate.accept(extensions['permessage-deflate']);
 
-      perMessageDeflate.compress(Buffer.from([1, 2, 3]), true, (err, compressed) => {
+      perMessageDeflate.compress(Buffer.from(srcData, 'utf8'), true, (err, compressed) => {
         if (err) return done(err);
 
         perMessageDeflate.decompress(compressed, true, (err, data) => {
           if (err) return done(err);
 
-          assert.ok(data.equals(Buffer.from([1, 2, 3])));
+          assert.ok(data.equals(Buffer.from(srcData, 'utf8')));
           done();
         });
       });
     });
 
+    it('should compress/decompress with level parameter', function (done) {
+      const perMessageDeflateLev9 = new PerMessageDeflate({
+        threshold: 0,
+        level: 9
+      });
+      const perMessageDeflateLev0 = new PerMessageDeflate({
+        threshold: 0,
+        level: 0
+      });
+      const extensionStr = (
+        'permessage-deflate; server_no_context_takeover; ' +
+        'client_no_context_takeover; server_max_window_bits=10; ' +
+        'client_max_window_bits=11'
+      );
+      const srcData = 'Some compressible data, it\'s compressible.';
+      const srcDataBuffer = Buffer.from(srcData, 'utf8');
+
+      perMessageDeflateLev0.accept(Extensions.parse(extensionStr)['permessage-deflate']);
+      perMessageDeflateLev9.accept(Extensions.parse(extensionStr)['permessage-deflate']);
+
+      perMessageDeflateLev0.compress(srcDataBuffer, true, (err, compressed1) => {
+        if (err) return done(err);
+
+        perMessageDeflateLev0.decompress(compressed1, true, (err, data1) => {
+          if (err) return done(err);
+
+          perMessageDeflateLev9.compress(srcDataBuffer, true, (err, compressed2) => {
+            if (err) return done(err);
+
+            perMessageDeflateLev9.decompress(compressed2, true, (err, data2) => {
+              if (err) return done(err);
+
+              // Level 0 compression actually adds a few bytes due to headers
+              assert.ok(compressed1.length > srcDataBuffer.length);
+              // Level 9 should not, of course.
+              assert.ok(compressed2.length < compressed1.length);
+              assert.ok(compressed2.length < srcDataBuffer.length);
+              // Ensure they both decompress back properly.
+              assert.ok(data1.equals(srcDataBuffer));
+              assert.ok(data2.equals(srcDataBuffer));
+              done();
+            });
+          });
+        });
+      });
+    });
+
     it('should compress/decompress data with no context takeover', function (done) {
       const perMessageDeflate = new PerMessageDeflate({ threshold: 0 });
       const extensions = Extensions.parse(