Skip to main content

Scrypt

This example demonstrates the performance benefits of using Piscina for CPU-intensive cryptographic operations, specifically the scrypt key derivation function. It compares four different implementations: pooled and unpooled versions, each with both synchronous and asynchronous variants.

Setup​

The monitor script measures event loop delay, helping us to understand the impact of each implementation on the main thread's responsiveness.

monitor.js
'use strict';

const { monitorEventLoopDelay } = require('perf_hooks');
const { isMainThread } = require('worker_threads');

if (!isMainThread) return;

const monitor = monitorEventLoopDelay({ resolution: 20 });

monitor.enable();

process.on('exit', () => {
monitor.disable();
console.log('Main Thread Mean/Max/99% Event Loop Delay:',
monitor.mean,
monitor.max,
monitor.percentile(99));
});

Pooled Asynchronous​

The pooled versions use Piscina to distribute the scrypt operations across multiple worker threads, potentially improving performance on multi-core systems.

pooled.js
'use strict';

const Piscina = require('piscina');
const { resolve } = require('path');
const crypto = require('crypto');
const { promisify } = require('util');
const randomFill = promisify(crypto.randomFill);
const { performance, PerformanceObserver } = require('perf_hooks');

const obs = new PerformanceObserver((entries) => {
console.log(entries.getEntries()[0].duration);
});
obs.observe({ entryTypes: ['measure'] });

const piscina = new Piscina({
filename: resolve(__dirname, 'scrypt.js'),
concurrentTasksPerWorker: 10
});

process.on('exit', () => {
const { runTime, waitTime } = piscina;
console.log('Run Time Average:', runTime.average);
console.log('Run Time Mean/Stddev:', runTime.mean, runTime.stddev);
console.log('Run Time Min:', runTime.min);
console.log('Run Time Max:', runTime.max);
console.log('Wait Time Average:', waitTime.average);
console.log('Wait Time Mean/Stddev:', waitTime.mean, waitTime.stddev);
console.log('Wait Time Min:', waitTime.min);
console.log('Wait Time Max:', waitTime.max);
});

async function * generateInput () {
let max = parseInt(process.argv[2] || 10);
const data = Buffer.allocUnsafe(10);
while (max-- > 0) {
yield randomFill(data);
}
}

(async function () {
performance.mark('start');
const keylen = 64;

for await (const input of generateInput()) {
await piscina.run({ input, keylen });
}

performance.mark('end');
performance.measure('start to end', 'start', 'end');
})();
scrypt.js
// eslint-disable no-unused-vars
'use strict';

const crypto = require('crypto');
const { promisify } = require('util');
const scrypt = promisify(crypto.scrypt);
const randomFill = promisify(crypto.randomFill);

const salt = Buffer.allocUnsafe(16);

module.exports = async function ({
input,
keylen,
N = 16384,
r = 8,
p = 1,
maxmem = 32 * 1024 * 1024
}) {
return (await scrypt(
input,
await randomFill(salt),
keylen, { N, r, p, maxmem })).toString('hex');
};

Unpooled Asynchronous​

The asynchronous versions use promisify versions of scrypt and randomFill, which don't block the event loop but may have slightly higher overhead.

unpooled.js
'use strict';

const crypto = require('crypto');
const { promisify } = require('util');
const randomFill = promisify(crypto.randomFill);
const scrypt = promisify(crypto.scrypt);
const { performance, PerformanceObserver } = require('perf_hooks');

const salt = Buffer.allocUnsafe(16);

const obs = new PerformanceObserver((entries) => {
console.log(entries.getEntries()[0].duration);
});
obs.observe({ entryTypes: ['measure'] });

async function * generateInput () {
let max = parseInt(process.argv[2] || 10);
const data = Buffer.allocUnsafe(10);
while (max-- > 0) {
yield randomFill(data);
}
}

(async function () {
performance.mark('start');
const keylen = 64;

for await (const input of generateInput()) {
(await scrypt(input, await randomFill(salt), keylen)).toString('hex');
}
performance.mark('end');
performance.measure('start to end', 'start', 'end');
})();

Pooled Synchronous​

The synchronous versions use scryptSync and randomFillSync, which can be more efficient but may block the event loop.

pooled_sync.js
'use strict';

const Piscina = require('../..');
const { resolve } = require('path');
const crypto = require('crypto');
const { promisify } = require('util');
const randomFill = promisify(crypto.randomFill);
const { performance, PerformanceObserver } = require('perf_hooks');

const obs = new PerformanceObserver((entries) => {
console.log(entries.getEntries()[0].duration);
});
obs.observe({ entryTypes: ['measure'] });

const piscina = new Piscina({
filename: resolve(__dirname, 'scrypt_sync.js')
});

process.on('exit', () => {
const { runTime, waitTime } = piscina;
console.log('Run Time Average:', runTime.average);
console.log('Run Time Mean/Stddev:', runTime.mean, runTime.stddev);
console.log('Run Time Min:', runTime.min);
console.log('Run Time Max:', runTime.max);
console.log('Wait Time Average:', waitTime.average);
console.log('Wait Time Mean/Stddev:', waitTime.mean, waitTime.stddev);
console.log('Wait Time Min:', waitTime.min);
console.log('Wait Time Max:', waitTime.max);
});

async function * generateInput () {
let max = parseInt(process.argv[2] || 10);
const data = Buffer.allocUnsafe(10);
while (max-- > 0) {
yield randomFill(data);
}
}

(async function () {
performance.mark('start');
const keylen = 64;

for await (const input of generateInput()) {
await piscina.run({ input, keylen });
}

performance.mark('end');
performance.measure('start to end', 'start', 'end');
})();
scrypt_sync.js
'use strict';

const { scryptSync, randomFillSync } = require('crypto');

const salt = Buffer.allocUnsafe(16);

module.exports = function ({
input,
keylen,
N = 16384,
r = 8,
p = 1,
maxmem = 32 * 1024 * 1024
}) {
return scryptSync(input,
randomFillSync(salt),
keylen,
{ N, r, p, maxmem }).toString('hex');
};

Unpooled Synchronous​

unpooled_sync.js
'use strict';

const crypto = require('crypto');
const { promisify } = require('util');
const { scryptSync, randomFillSync } = crypto;
const randomFill = promisify(crypto.randomFill);
const { performance, PerformanceObserver } = require('perf_hooks');

const salt = Buffer.allocUnsafe(16);

const obs = new PerformanceObserver((entries) => {
console.log(entries.getEntries()[0].duration);
});
obs.observe({ entryTypes: ['measure'] });

async function * generateInput () {
let max = parseInt(process.argv[2] || 10);
const data = Buffer.allocUnsafe(10);
while (max-- > 0) {
yield randomFill(data);
}
}

(async function () {
performance.mark('start');
const keylen = 64;

for await (const input of generateInput()) {
// Everything in here is intentionally sync
scryptSync(input, randomFillSync(salt), keylen).toString('hex');
}
performance.mark('end');
performance.measure('start to end', 'start', 'end');
})();

Running the Example​

The package.json file includes scripts to run each variant of the scrypt implementation with the monitor.

package.json
{
"name": "scrypt",
"version": "1.0.0",
"scripts": {
"pooled": "node -r ./monitor pooled",
"unpooled": "node -r ./monitor unpooled",
"pooled-sync": "node -r ./monitor pooled_sync",
"unpooled-sync": "node -r ./monitor unpooled_sync"
},
"keywords": [],
"author": "",
"license": "MIT",
"description": ""
}

To run the different implementations and compare their performance:

npm run pooled 100
npm run unpooled 100
npm run pooled-sync 100
npm run unpooled-sync 100

You can also check out this example on github.