bench_bcrypt.mjs 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import minimist from 'minimist'
  2. import { promisify } from 'util'
  3. import bcrypt from 'bcrypt'
  4. import { promiseMapWithLimit } from '@overleaf/promise-utils'
  5. // https://github.com/import-js/eslint-plugin-import/issues/1810
  6. // eslint-disable-next-line import/no-unresolved
  7. import * as csv from 'csv/sync'
  8. const bcryptCompare = promisify(bcrypt.compare)
  9. const bcryptGenSalt = promisify(bcrypt.genSalt)
  10. const bcryptHash = promisify(bcrypt.hash)
  11. const argv = minimist(process.argv.slice(2), {
  12. string: ['major', 'minor', 'concurrency', 'samples', 'password'],
  13. bool: ['hash', 'compare', 'verbose', 'table', 'csv'],
  14. default: {
  15. major: '12,13,14,15',
  16. minor: 'a',
  17. concurrency: '1,2,4,10,20',
  18. samples: 100,
  19. password: 'x'.repeat(72),
  20. hash: true,
  21. compare: true,
  22. verbose: true,
  23. table: true,
  24. csv: true,
  25. },
  26. })
  27. const SAMPLES = parseInt(argv.samples, 10)
  28. const STATS = []
  29. function asListOfInt(s) {
  30. return s.split(',').map(x => parseInt(x, 10))
  31. }
  32. async function computeHash(rounds, minor) {
  33. const salt = await bcryptGenSalt(rounds, minor)
  34. return await bcryptHash(argv.password, salt)
  35. }
  36. async function sample(concurrency, fn) {
  37. const stats = await promiseMapWithLimit(
  38. concurrency,
  39. new Array(SAMPLES).fill(0),
  40. async () => {
  41. const t0 = process.hrtime.bigint()
  42. await fn()
  43. const t1 = process.hrtime.bigint()
  44. return Number(t1 - t0) / 1e6
  45. }
  46. )
  47. const sum = stats.reduce((a, b) => a + b, 0)
  48. const avg = sum / SAMPLES
  49. stats.sort((a, b) => a - b)
  50. const median = stats[Math.ceil(SAMPLES / 2)]
  51. const p95 = stats[Math.ceil(SAMPLES * 0.95)]
  52. const min = stats[0]
  53. const max = stats[stats.length - 1]
  54. return Object.fromEntries(
  55. Object.entries({
  56. min,
  57. avg,
  58. median,
  59. p95,
  60. max,
  61. }).map(([key, value]) => [key, Math.ceil(value) + 'ms'])
  62. )
  63. }
  64. async function run(rounds, minor, concurrency) {
  65. if (argv.hash) {
  66. const stats = await sample(concurrency, async () => {
  67. await computeHash(rounds, minor)
  68. })
  69. STATS.push({
  70. kind: 'hash',
  71. rounds,
  72. concurrency,
  73. ...stats,
  74. })
  75. if (argv.verbose) console.log(STATS[STATS.length - 1])
  76. }
  77. if (argv.compare) {
  78. const hashedPassword = await computeHash(rounds, minor)
  79. const stats = await sample(concurrency, async () => {
  80. await bcryptCompare(argv.password, hashedPassword)
  81. })
  82. STATS.push({
  83. kind: 'compare',
  84. rounds,
  85. concurrency,
  86. ...stats,
  87. })
  88. if (argv.verbose) console.log(STATS[STATS.length - 1])
  89. }
  90. }
  91. async function main() {
  92. for (const rounds of asListOfInt(argv.major)) {
  93. for (const minor of argv.minor.split(',')) {
  94. for (const concurrency of asListOfInt(argv.concurrency)) {
  95. await run(rounds, minor, concurrency)
  96. }
  97. }
  98. }
  99. STATS.forEach(s => {
  100. s.samples = SAMPLES
  101. })
  102. if (argv.table) console.table(STATS)
  103. if (argv.csv) console.log(csv.stringify(STATS, { header: true }))
  104. }
  105. try {
  106. await main()
  107. process.exit(0)
  108. } catch (error) {
  109. console.error(error)
  110. process.exit(1)
  111. }