Merge pull request #14579 from overleaf/jpa-bench-bcrypt

[web] add script for benchmarking bcrypt performance

GitOrigin-RevId: c87ef9485323630ddb10b3bed2ed64f8f6812541
This commit is contained in:
Jakob Ackermann 2023-08-31 12:19:04 +02:00 committed by Copybot
parent e23c2dafef
commit 0c767f67c9

View file

@ -0,0 +1,121 @@
const minimist = require('minimist')
const { promisify } = require('util')
const bcrypt = require('bcrypt')
const { promiseMapWithLimit } = require('../app/src/util/promises')
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)
})