├── .editorconfig ├── .gitignore ├── .jscsrc ├── .jshintrc ├── LICENSE.md ├── README.md ├── app.js ├── package.json └── public ├── index.html ├── main.css ├── main.js └── workers ├── json-channel.js ├── json.js ├── strings-channel.js ├── strings.js ├── test.js ├── transfer-channel.js └── transfer.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.hbs] 21 | insert_final_newline = false 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.css] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.html] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | [*.{diff,md}] 34 | trim_trailing_whitespace = false 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ember-suave" 3 | } 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esnext": true, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runspired/webworker-performance/937826c845b53b602a70924c95809dc07e9d2e7e/LICENSE.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebWorker Performance Tests 2 | 3 | This repo tests the message passing speed between the main thread and a worker thread 4 | for each of the following situations as available: 5 | 6 | - strings: `Worker.postMessage()` 7 | - json transfer/structured cloning: `Worker.postMessage()` 8 | - transferable objects: `Worker.postMessage(, [transferables])` 9 | 10 | - channel strings: `channel.postMessage()` 11 | - channel json transfer/structured cloning: `channel.postMessage()` 12 | - channel transferable objects: `channel.postMessage(, [transferables])` 13 | 14 | Each test is run with 2 data setups. 15 | 16 | Also note, many data formats can be used only when not using `JSON.stringify` unless 17 | a bespoke implementation of stringify is created. For instance, stringify misses out 18 | in these areas: 19 | 20 | - duplicating/transferring RegExp objects. 21 | - duplicating/transferring Blob, File, and FileList objects. 22 | - duplicating/transferring ImageData objects. The dimensions of the clone's CanvasPixelArray will match the original and have a duplicate of the same pixel data. 23 | - duplicating/transferring objects containing cyclic graphs of references. 24 | 25 | This means that even in cases where using `JSON.stringify` is marginally faster, once these 26 | cases are accounted for it's likely to be slower. 27 | 28 | Each test measures round trips: e.g. the time it takes to serialize, send, receive a response, 29 | and parse the response. This means this test is biased towards operations that expect a response, 30 | while many good setups communicate only worker to main thread. A fair test would test 31 | the main thread overhead of processing a response. 32 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | 4 | app.use(express.static('public')); 5 | app.use(express.static('node_modules')); 6 | 7 | app.listen(4200, function () { 8 | console.log('Express App listening on port 4200!'); 9 | }); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webworker-performance", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "rsvp": "3.2.1" 6 | }, 7 | "devDependencies": { 8 | "express": "^4.13.4" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebWorker Performance Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Worker Performance Tests

13 |
14 |
15 |

Setup

16 |

17 | These represent the available features. 18 |

19 |
    20 |
    21 |
    22 |

    Test Results

    23 |

    24 | These tests can take up to 2 or 3 minutes to complete depending on your browser. 25 | Times are measured with `window.performance.now` and averaged over 1000 runs. 26 | Each test suite is run 10x. 27 |

    28 |

    29 | The "Simple" test has a payload of 100 strings in an array within a wrapper object. 30 | The "Complex" test has a payload of 100 objects in an array within a wrapper object. 31 |

    32 |

    33 | Each test measures round trips: e.g. the time it takes to serialize, send, receive a 34 | response, and parse the response. This means this test is biased towards operations 35 | that expect a response, while many good setups communicate only worker to main thread. 36 | A fair test would test the main thread overhead of processing a response. 37 |

    38 |

    Final Results

    39 |
      40 |

      Per Run Results

      41 |
        42 |
        43 |
        44 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /public/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runspired/webworker-performance/937826c845b53b602a70924c95809dc07e9d2e7e/public/main.css -------------------------------------------------------------------------------- /public/main.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var SYSTEM_QUERY_TYPE = '-system-query'; 3 | var PAYLOAD_TYPE = '-payload'; 4 | var GUID = 1; 5 | var TOTAL_RUNS = 10; 6 | 7 | function Payload(data) { 8 | this.type = PAYLOAD_TYPE; 9 | this.id = 'ui-' + (GUID++); 10 | this.data = data; 11 | } 12 | 13 | function PayloadData(type, i) { 14 | this.name = 'worker-test-obj-' + i; 15 | var data = []; 16 | var j; 17 | if (type === 'simple') { 18 | for (j = 0; j < 100; j++) { 19 | data.push('payload-item-' + j); 20 | } 21 | } else { 22 | for (j = 0; j < 100; j++) { 23 | data.push({ name: 'payload-item-' + j, value: j }); 24 | } 25 | } 26 | this.data = data; 27 | } 28 | 29 | function WorkerTest(options) { 30 | this.worker = new Worker('./workers/test.js'); 31 | this.log = options.log; 32 | this.runners = { 33 | 'json': new JsonTest('json', options.log), 34 | 'json-channel': new ChannelJsonTest('json-channel', options.log), 35 | 'strings': new StringTest('strings', options.log), 36 | 'strings-channel': new ChannelStringTest('strings-channel', options.log), 37 | 'transfer': new TransferTest('transfer', options.log), 38 | 'transfer-channel': new ChannelTransferTest('transfer-channel', options.log) 39 | }; 40 | 41 | this.run(); 42 | } 43 | 44 | WorkerTest.prototype._send = function _sendWorkerData(data, buffers) { 45 | if (buffers) { 46 | this.worker.postMessage(data, buffers); 47 | } else { 48 | this.worker.postMessage(data); 49 | } 50 | }; 51 | 52 | WorkerTest.prototype.detectFeatures = function _detectWorkerFeatures() { 53 | var _test = this; 54 | return new RSVP.Promise(function(resolve) { 55 | var features = { 56 | strings: true, 57 | json: false, 58 | cloning: false, 59 | transfer: false, 60 | channels: false 61 | }; 62 | 63 | // check JSON transfer 64 | try { 65 | _test._send({ 66 | type: SYSTEM_QUERY_TYPE, 67 | name: 'json-transfer', 68 | data: { name: 'JSON usability test' } 69 | }); 70 | features.json = true; 71 | } catch (e) { 72 | // Worker does not support anything but strings 73 | } 74 | 75 | if (features.json) { 76 | //detect Structured Cloning and Transferable Objects 77 | if (typeof ArrayBuffer !== 'undefined') { 78 | try { 79 | const ab = new ArrayBuffer(1); 80 | 81 | _test._send({ 82 | type: SYSTEM_QUERY_TYPE, 83 | name: 'buffer-transfer', 84 | data: ab 85 | }, [ab]); 86 | 87 | // if the byteLength is 0, the content of the buffer was transferred 88 | features.transfer = !ab.byteLength; 89 | features.cloning = !features.transfer; 90 | 91 | } catch (e) { 92 | // neither feature is available 93 | } 94 | } 95 | } 96 | 97 | // check channels 98 | if (typeof MessageChannel !== 'undefined') { 99 | features.channels = true; 100 | } 101 | 102 | resolve(features); 103 | }); 104 | }; 105 | 106 | WorkerTest.prototype.logFinalResults = function logFinalResults(test, type) { 107 | 108 | var avg = test.runs[type].reduce(function(v, c) { 109 | return v + c; 110 | }, 0); 111 | 112 | this.log({ 113 | type: 'final-result', 114 | data: { 115 | type: type, 116 | label: test.name, 117 | value: (avg / 10) + 'ms' 118 | } 119 | }); 120 | }; 121 | 122 | WorkerTest.prototype.logTestResults = function logTestResults(test, type) { 123 | var avg = Object.keys(test.trips).reduce(function(value, trip) { 124 | return value + test.trips[trip].getDuration(); 125 | }, 0); 126 | 127 | this.log({ 128 | type: 'result', 129 | data: { 130 | type: type, 131 | label: test.name, 132 | value: (avg / 1000) + 'ms' 133 | } 134 | }); 135 | 136 | // clean up 137 | test.runs[type].push((avg / 1000)); 138 | 139 | if (test.runs[type].length === TOTAL_RUNS) { 140 | this.logFinalResults(test, type); 141 | } 142 | 143 | test.trips = {}; 144 | 145 | return true; 146 | }; 147 | 148 | WorkerTest.prototype.runTest = function _testWorker(test, type) { 149 | var _tests = this; 150 | var runs = []; 151 | var i; 152 | for (i = 1; i <= 1000; i++) { 153 | runs.push(i); 154 | } 155 | 156 | return runs.reduce(function(chain, run) { 157 | return chain 158 | .then(function() { return test.send(new PayloadData(type, run)); }); 159 | }, test.ready) 160 | .then(_tests.logTestResults.bind(_tests, test, type)) 161 | .catch(function(e) { return true; /* keep testing */ }); 162 | }; 163 | 164 | WorkerTest.prototype.chainTests = function _chainTests(tests, type) { 165 | var _test = this; 166 | return tests.reduce(function(chain, test) { 167 | return chain 168 | .then(_test.runTest.bind(_test, test, type)); 169 | }, RSVP.Promise.resolve()); 170 | }; 171 | 172 | WorkerTest.prototype.runOnce = function(tests) { 173 | return this.chainTests(tests, 'simple') 174 | .then(this.chainTests.bind(this, tests, 'complex')); 175 | }; 176 | 177 | WorkerTest.prototype.run = function _runWorkerTests() { 178 | var _test = this; 179 | this.detectFeatures() 180 | .then(function(features) { 181 | 182 | // print the compat matrix 183 | Object.keys(features).forEach(function(f) { 184 | _test.log({ 185 | type: 'feature', 186 | data: { 187 | label: f, 188 | value: features[f] ? 'true' : 'false' 189 | } 190 | }); 191 | }); 192 | 193 | // generate test scenarios 194 | var tests = []; 195 | 196 | // basic scenarios 197 | Object.keys(features).forEach(function(f) { 198 | if (features[f] && ['cloning', 'channels'].indexOf(f) === -1) { 199 | tests.push(_test.runners[f]); 200 | } 201 | }); 202 | 203 | // channel scenarios 204 | if (features['channels']) { 205 | Object.keys(features).forEach(function(f) { 206 | if (features[f] && ['cloning', 'channels'].indexOf(f) === -1) { 207 | tests.push(_test.runners[f + '-channel']); 208 | } 209 | }); 210 | } 211 | 212 | var runs = []; 213 | for (var i = 0; i < TOTAL_RUNS; i++) { 214 | runs.push(i); 215 | } 216 | return runs.reduce(function(chain) { 217 | return chain.then(function() { 218 | return _test.runOnce(tests); 219 | }); 220 | }, RSVP.Promise.resolve()); 221 | }); 222 | }; 223 | 224 | // export this 225 | this.WorkerTest = WorkerTest; 226 | 227 | 228 | 229 | 230 | 231 | 232 | function Trip(id) { 233 | this.id = id; 234 | this._startTime = null; 235 | this._endTime = null; 236 | this._isComplete = RSVP.defer(); 237 | 238 | var _trip = this; 239 | this.start = function() { 240 | if (!_trip._startTime) { 241 | _trip._startTime = window.performance.now(); 242 | } 243 | }; 244 | this.stop = function() { 245 | if (!_trip._endTime) { 246 | _trip._endTime = window.performance.now(); 247 | _trip._isComplete.resolve(); 248 | } 249 | }; 250 | this.getDuration = function() { 251 | return _trip._endTime - _trip._startTime; 252 | }; 253 | this.isComplete = this._isComplete.promise; 254 | } 255 | 256 | 257 | 258 | 259 | 260 | 261 | /* 262 | Base Test Class 263 | */ 264 | function Test(name, log) { 265 | this.log = log; 266 | this.name = name; 267 | this.worker = new Worker('./workers/' + name + '.js'); 268 | this.trips = {}; 269 | this.runs = { 270 | simple: [], 271 | complex: [] 272 | }; 273 | 274 | this._ready = RSVP.defer(); 275 | this.ready = this._ready.promise; 276 | this.setup(); 277 | } 278 | Test.prototype.setup = function() { 279 | var _self = this; 280 | this.worker.onmessage = function() { 281 | _self.worker.onmessage = function(msg, opts) { 282 | _self.handle(msg, opts); 283 | }; 284 | _self._ready.resolve(); 285 | }; 286 | if (this.name.indexOf('transfer') !== -1) { 287 | var buf = createTransferable({ hello: 'world' }); 288 | this.worker.postMessage(buf, [buf]); 289 | } else { 290 | this.worker.postMessage(JSON.stringify({ hello: 'world' })); 291 | } 292 | }; 293 | Test.prototype.initSend = function(data) { 294 | var request = new Payload(data); 295 | var trip = new Trip(request.id); 296 | this.trips[request.id] = trip; 297 | trip.start(); 298 | return request; 299 | }; 300 | Test.prototype.complete = function(data) { 301 | var trip = this.trips[data.id]; 302 | trip.stop(); 303 | }; 304 | 305 | 306 | function ChannelTest() { 307 | Test.apply(this, arguments); 308 | } 309 | ChannelTest.prototype = Object.create(Test.prototype); 310 | ChannelTest.prototype.setup = function() { 311 | var _self = this; 312 | this.channel = new MessageChannel(); 313 | this.channel.port1.onmessage = function() { 314 | if (_self.name === 'transfer-channel') { 315 | _self.log({ 316 | type: 'feature', 317 | data: { 318 | label: 'Channel + Transferables', 319 | value: 'true' 320 | } 321 | }); 322 | } 323 | 324 | _self.channel.port1.onmessage = function(msg, opts) { 325 | _self.handle(msg, opts); 326 | }; 327 | _self._ready.resolve(); 328 | }; 329 | 330 | this.worker.postMessage('Sending Channel', [this.channel.port2]); 331 | this.worker.onerror = function() { 332 | if (_self.name === 'transfer-channel') { 333 | _self.log({ 334 | type: 'feature', 335 | data: { 336 | label: 'Channel + Transferables', 337 | value: 'false' 338 | } 339 | }); 340 | _self._ready.reject(); 341 | } 342 | }; 343 | 344 | if (this.name.indexOf('transfer') !== -1) { 345 | var buf = createTransferable({ hello: 'world' }); 346 | this.channel.port1.postMessage(buf, [buf]); 347 | } else { 348 | this.channel.port1.postMessage(JSON.stringify({ hello: 'world' })); 349 | } 350 | }; 351 | 352 | 353 | // String tests 354 | function StringTest() { 355 | Test.apply(this, arguments); 356 | } 357 | StringTest.prototype = Object.create(Test.prototype); 358 | StringTest.prototype.send = function(data) { 359 | var payload = this.initSend(data); 360 | this.worker.postMessage(JSON.stringify(payload)); 361 | return this.trips[payload.id].isComplete; 362 | }; 363 | StringTest.prototype.handle = function(msg) { 364 | var data = JSON.parse(msg.data); 365 | this.complete(data); 366 | }; 367 | 368 | function ChannelStringTest() { 369 | ChannelTest.apply(this, arguments); 370 | } 371 | ChannelStringTest.prototype = Object.create(ChannelTest.prototype); 372 | ChannelStringTest.prototype.send = function(data) { 373 | var payload = this.initSend(data); 374 | this.channel.port1.postMessage(JSON.stringify(payload)); 375 | return this.trips[payload.id].isComplete; 376 | }; 377 | ChannelStringTest.prototype.handle = function(msg) { 378 | var data = JSON.parse(msg.data); 379 | this.complete(data); 380 | }; 381 | 382 | // Json tests 383 | function JsonTest() { 384 | Test.apply(this, arguments); 385 | } 386 | JsonTest.prototype = Object.create(Test.prototype); 387 | JsonTest.prototype.send = function(data) { 388 | var payload = this.initSend(data); 389 | this.worker.postMessage(payload); 390 | return this.trips[payload.id].isComplete; 391 | }; 392 | JsonTest.prototype.handle = function(msg) { 393 | this.complete(msg.data); 394 | }; 395 | 396 | function ChannelJsonTest() { 397 | ChannelTest.apply(this, arguments); 398 | } 399 | ChannelJsonTest.prototype = Object.create(ChannelTest.prototype); 400 | ChannelJsonTest.prototype.send = function(data) { 401 | var payload = this.initSend(data); 402 | this.channel.port1.postMessage(payload); 403 | return this.trips[payload.id].isComplete; 404 | }; 405 | ChannelJsonTest.prototype.handle = function(msg) { 406 | this.complete(msg.data); 407 | }; 408 | 409 | // Transfer tests 410 | function TransferTest() { 411 | Test.apply(this, arguments); 412 | } 413 | TransferTest.prototype = Object.create(Test.prototype); 414 | TransferTest.prototype.send = function(data) { 415 | var payload = this.initSend(data); 416 | var buf = createTransferable(payload); 417 | this.worker.postMessage(buf, [buf]); 418 | return this.trips[payload.id].isComplete; 419 | }; 420 | TransferTest.prototype.handle = function(msg) { 421 | this.complete(expandTransferable(msg.data)); 422 | }; 423 | 424 | function ChannelTransferTest() { 425 | ChannelTest.apply(this, arguments); 426 | } 427 | ChannelTransferTest.prototype = Object.create(ChannelTest.prototype); 428 | ChannelTransferTest.prototype.send = function(data) { 429 | var payload = this.initSend(data); 430 | var buf = createTransferable(payload); 431 | this.channel.port1.postMessage(buf, [buf]); 432 | return this.trips[payload.id].isComplete; 433 | }; 434 | ChannelTransferTest.prototype.handle = function(msg) { 435 | this.complete(expandTransferable(msg.data)); 436 | }; 437 | 438 | 439 | function expandTransferable(buf) { 440 | var str = String.fromCharCode.apply(null, new Uint16Array(buf)); 441 | return JSON.parse(str); 442 | } 443 | 444 | function createTransferable(o) { 445 | var str = JSON.stringify(o); 446 | var buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char 447 | var bufView = new Uint16Array(buf); 448 | for (var i = 0, strLen = str.length; i < strLen; i++) { 449 | bufView[i] = str.charCodeAt(i); 450 | } 451 | return buf; 452 | } 453 | 454 | })(window); 455 | -------------------------------------------------------------------------------- /public/workers/json-channel.js: -------------------------------------------------------------------------------- 1 | self.setupPort = function() { 2 | self.port.onmessage = function(e) { 3 | self.port.postMessage(e.data); 4 | } 5 | }; 6 | 7 | self.onmessage = function(msg) { 8 | if (msg.ports[0]) { 9 | self.port = msg.ports[0]; 10 | self.setupPort(); 11 | self.onmessage = null; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /public/workers/json.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('message', function (e) { 2 | self.postMessage(e.data); 3 | }); 4 | -------------------------------------------------------------------------------- /public/workers/strings-channel.js: -------------------------------------------------------------------------------- 1 | self.setupPort = function() { 2 | self.port.onmessage = function(e) { 3 | self.port.postMessage(JSON.stringify(JSON.parse(e.data))); 4 | } 5 | }; 6 | 7 | self.onmessage = function(msg) { 8 | if (msg.ports[0]) { 9 | self.port = msg.ports[0]; 10 | self.setupPort(); 11 | self.onmessage = null; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /public/workers/strings.js: -------------------------------------------------------------------------------- 1 | self.onmessage = function(msg) { 2 | self.postMessage(JSON.stringify(JSON.parse(msg.data))); 3 | }; 4 | -------------------------------------------------------------------------------- /public/workers/test.js: -------------------------------------------------------------------------------- 1 | self.onmessage = function(msg) { 2 | console.log('worker msg received', msg); 3 | }; 4 | -------------------------------------------------------------------------------- /public/workers/transfer-channel.js: -------------------------------------------------------------------------------- 1 | self.setupPort = function() { 2 | self.port.onmessage = function(msg) { 3 | try { 4 | var buf = createTransferable(expandTransferable(msg.data)); 5 | self.port.postMessage(buf, [buf]); 6 | } catch (e) { 7 | throw e; 8 | } 9 | } 10 | }; 11 | 12 | self.onmessage = function(msg) { 13 | if (msg.ports[0]) { 14 | self.port = msg.ports[0]; 15 | self.setupPort(); 16 | self.onmessage = null; 17 | } 18 | }; 19 | 20 | function expandTransferable(buf) { 21 | var str = String.fromCharCode.apply(null, new Uint16Array(buf)); 22 | return JSON.parse(str); 23 | } 24 | 25 | function createTransferable(o) { 26 | var str = JSON.stringify(o); 27 | var buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char 28 | var bufView = new Uint16Array(buf); 29 | for (var i = 0, strLen = str.length; i < strLen; i++) { 30 | bufView[i] = str.charCodeAt(i); 31 | } 32 | return buf; 33 | } 34 | -------------------------------------------------------------------------------- /public/workers/transfer.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('message', function (e) { 2 | var buf = createTransferable(expandTransferable(e.data)); 3 | self.postMessage(buf, [buf]); 4 | }); 5 | 6 | function expandTransferable(buf) { 7 | var str = String.fromCharCode.apply(null, new Uint16Array(buf)); 8 | return JSON.parse(str); 9 | } 10 | 11 | function createTransferable(o) { 12 | var str = JSON.stringify(o); 13 | var buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char 14 | var bufView = new Uint16Array(buf); 15 | for (var i = 0, strLen = str.length; i < strLen; i++) { 16 | bufView[i] = str.charCodeAt(i); 17 | } 18 | return buf; 19 | } 20 | --------------------------------------------------------------------------------