From 3229d748876729718edc3d4c100575e7a73525a8 Mon Sep 17 00:00:00 2001 From: Yanis Benson Date: Fri, 3 May 2019 09:48:26 +0300 Subject: [PATCH] Move hashing to worker thread (#7) --- .travis.yml | 1 + index.js | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 10 ++++++-- readme.md | 2 +- test.js | 14 +++++++++++ thread.js | 12 +++++++++ 6 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 thread.js diff --git a/.travis.yml b/.travis.yml index f3fa8cd..f98fed0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: node_js node_js: + - '12' - '10' - '8' diff --git a/index.js b/index.js index eeaeb50..cf7367d 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,50 @@ 'use strict'; const crypto = require('crypto'); -const create = algorithm => async (buffer, options) => { +const requireOptional = (name, defaultValue) => { + try { + return require(name); + } catch (_) { + return defaultValue; + } +}; + +const {Worker} = requireOptional('worker_threads', {}); + +let worker; // Lazy +let taskIdCounter = 0; +const tasks = new Map(); + +const createWorker = () => { + worker = new Worker('./thread.js'); + worker.on('message', message => { + const task = tasks.get(message.id); + tasks.delete(message.id); + if (tasks.size === 0) { + worker.unref(); + } + + task(message.value); + }); + + worker.on('error', error => { + // Any error here is effectively an equivalent of segfault and have no scope, so we just throw it on callback level + throw error; + }); +}; + +const taskWorker = (value, transferList) => new Promise(resolve => { + const id = taskIdCounter++; + tasks.set(id, resolve); + + if (worker === undefined) { + createWorker(); + } + + worker.postMessage({id, value}, transferList); +}); + +let create = algorithm => async (buffer, options) => { options = { outputFormat: 'hex', ...options @@ -17,6 +60,32 @@ const create = algorithm => async (buffer, options) => { return hash.digest().buffer; }; +if (Worker !== undefined) { + create = algorithm => async (source, options) => { + options = { + outputFormat: 'hex', + ...options + }; + + let buffer; + if (typeof source === 'string') { + // Saving one copy operation by writing string to buffer right away and then transfering buffer + buffer = new ArrayBuffer(Buffer.byteLength(source, 'utf8')); + Buffer.from(buffer).write(source, 'utf8'); + } else { + // Creating a copy of buffer at call time, will be transfered later + buffer = source.buffer.slice(0); + } + + const result = await taskWorker({algorithm, buffer}, [buffer]); + if (options.outputFormat === 'hex') { + return Buffer.from(result).toString('hex'); + } + + return result; + }; +} + exports.sha1 = create('sha1'); exports.sha256 = create('sha256'); exports.sha384 = create('sha384'); diff --git a/package.json b/package.json index d3a9751..f08d750 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "files": [ "index.js", "index.d.ts", - "browser.js" + "browser.js", + "thread.js" ], "keywords": [ "crypto", @@ -41,5 +42,10 @@ "tsd": "^0.7.2", "xo": "^0.24.0" }, - "browser": "browser.js" + "browser": "browser.js", + "xo": { + "rules": { + "import/no-unresolved": "off" + } + } } diff --git a/readme.md b/readme.md index 90afd68..34d6c2b 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ const {sha256} = require('crypto-hash'); Returns a `Promise` with a hex-encoded hash. -*Note that even though it returns a promise, [in Node.js, the operation is synchronous 💩](https://github.com/nodejs/node/issues/678).* +*In Node.js 12 or later, the operation is executed using [`worker_threads`](https://nodejs.org/api/worker_threads.html). A thread is lazily spawned on the first operation and lives until the end of the program execution. It's `unref`ed, so it won't keep the process alive.* [SHA-1 is insecure](https://stackoverflow.com/a/38045085/64949) and should not be used for anything sensitive. diff --git a/test.js b/test.js index 1079524..c2f3dac 100644 --- a/test.js +++ b/test.js @@ -34,3 +34,17 @@ test('buffer output', async t => { const result = await sha1('🦄', {outputFormat: 'buffer'}); t.is(is(result), 'ArrayBuffer'); }); + +test('parallel execution', async t => { + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(sha512('🦄')); + } + + const results = await Promise.all(promises); + t.is(results.length, promises.length); + + for (const result of results) { + t.is(result, '7d9e515c59bd15d0692f9bc0c68f50f82b62a99bef4b8dc490cec165296210dff005529a4cb84a655eee6ddec82339e6bdbab21bdb287b71a543a56cfab53905'); + } +}); diff --git a/thread.js b/thread.js new file mode 100644 index 0000000..eaac620 --- /dev/null +++ b/thread.js @@ -0,0 +1,12 @@ +'use strict'; +const crypto = require('crypto'); +const {parentPort} = require('worker_threads'); + +parentPort.on('message', message => { + const {algorithm, buffer} = message.value; + const hash = crypto.createHash(algorithm); + hash.update(Buffer.from(buffer)); + const arrayBuffer = hash.digest().buffer; + // Transfering buffer here for consistency, but considering buffer size it might be faster to just leave it for copying, needs perf test + parentPort.postMessage({id: message.id, value: arrayBuffer}, [arrayBuffer]); +});