const minimist = require('minimist') const { promisify } = require('util') const bcrypt = require('bcrypt') const { promiseMapWithLimit } = require('@overleaf/promise-utils') const csv = require('csv/sync') const bcryptCompare = promisify(bcrypt.compare) const bcryptGenSalt = promisify(bcrypt.genSalt) const bcryptHash = promisify(bcrypt.hash) const argv = minimist(process.argv.slice(2), { string: ['major', 'minor', 'concurrency', 'samples', 'password'], bool: ['hash', 'compare', 'verbose', 'table', 'csv'], default: { major: '12,13,14,15', minor: 'a', concurrency: '1,2,4,10,20', samples: 100, password: 'x'.repeat(72), hash: true, compare: true, verbose: true, table: true, csv: true, }, }) const SAMPLES = parseInt(argv.samples, 10) const STATS = [] function asListOfInt(s) { return s.split(',').map(x => parseInt(x, 10)) } async function computeHash(rounds, minor) { const salt = await bcryptGenSalt(rounds, minor) return await bcryptHash(argv.password, salt) } async function sample(concurrency, fn) { const stats = await promiseMapWithLimit( concurrency, new Array(SAMPLES).fill(0), async () => { const t0 = process.hrtime.bigint() await fn() const t1 = process.hrtime.bigint() return Number(t1 - t0) / 1e6 } ) const sum = stats.reduce((a, b) => a + b, 0) const avg = sum / SAMPLES stats.sort((a, b) => a - b) const median = stats[Math.ceil(SAMPLES / 2)] const p95 = stats[Math.ceil(SAMPLES * 0.95)] const min = stats[0] const max = stats[stats.length - 1] return Object.fromEntries( Object.entries({ min, avg, median, p95, max, }).map(([key, value]) => [key, Math.ceil(value) + 'ms']) ) } async function run(rounds, minor, concurrency) { if (argv.hash) { const stats = await sample(concurrency, async () => { await computeHash(rounds, minor) }) STATS.push({ kind: 'hash', rounds, concurrency, ...stats, }) if (argv.verbose) console.log(STATS[STATS.length - 1]) } if (argv.compare) { const hashedPassword = await computeHash(rounds, minor) const stats = await sample(concurrency, async () => { await bcryptCompare(argv.password, hashedPassword) }) STATS.push({ kind: 'compare', rounds, concurrency, ...stats, }) if (argv.verbose) console.log(STATS[STATS.length - 1]) } } async function main() { for (const rounds of asListOfInt(argv.major)) { for (const minor of argv.minor.split(',')) { for (const concurrency of asListOfInt(argv.concurrency)) { await run(rounds, minor, concurrency) } } } STATS.forEach(s => { s.samples = SAMPLES }) if (argv.table) console.table(STATS) if (argv.csv) console.log(csv.stringify(STATS, { header: true })) } main() .then(() => { process.exit(0) }) .catch(err => { console.error(err) process.exit(1) })