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.