Skip to content

Commit

Permalink
Move hashing to worker thread (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yanis Benson authored and sindresorhus committed May 3, 2019
1 parent 926223f commit 3229d74
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 4 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
language: node_js
node_js:
- '12'
- '10'
- '8'
71 changes: 70 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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');
Expand Down
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"files": [
"index.js",
"index.d.ts",
"browser.js"
"browser.js",
"thread.js"
],
"keywords": [
"crypto",
Expand All @@ -41,5 +42,10 @@
"tsd": "^0.7.2",
"xo": "^0.24.0"
},
"browser": "browser.js"
"browser": "browser.js",
"xo": {
"rules": {
"import/no-unresolved": "off"
}
}
}
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const {sha256} = require('crypto-hash');

Returns a `Promise<string>` 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.

Expand Down
14 changes: 14 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
12 changes: 12 additions & 0 deletions thread.js
Original file line number Diff line number Diff line change
@@ -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]);
});

0 comments on commit 3229d74

Please sign in to comment.