Skip to content

Commit 499bed1

Browse files
committed
crypto: add buffering to randomInt
1 parent 568b26a commit 499bed1

File tree

2 files changed

+95
-25
lines changed

2 files changed

+95
-25
lines changed

benchmark/crypto/randomInt.js

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const { randomInt } = require('crypto');
5+
6+
const bench = common.createBenchmark(main, {
7+
mode: ['sync', 'async-sequential', 'async-parallel'],
8+
min: [-(2 ** 47) + 1, -10_000, -100],
9+
max: [100, 10_000, 2 ** 47],
10+
n: [1e3, 1e5]
11+
});
12+
13+
function main({ mode, min, max, n }) {
14+
if (mode === 'sync') {
15+
bench.start();
16+
for (let i = 0; i < n; i++)
17+
randomInt(min, max);
18+
bench.end(n);
19+
} else if (mode === 'async-sequential') {
20+
bench.start();
21+
(function next(i) {
22+
if (i === n)
23+
return bench.end(n);
24+
randomInt(min, max, () => {
25+
next(i + 1);
26+
});
27+
})(0);
28+
} else {
29+
bench.start();
30+
let done = 0;
31+
for (let i = 0; i < n; i++) {
32+
randomInt(min, max, () => {
33+
if (++done === n)
34+
bench.end(n);
35+
});
36+
}
37+
}
38+
}

lib/internal/crypto/random.js

+57-25
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@ function randomFill(buf, offset, size, callback) {
179179
// e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6);
180180
const RAND_MAX = 0xFFFF_FFFF_FFFF;
181181

182+
// Cache random data to use in randomInt. The cache size must be evenly
183+
// divisible by 6 because each attempt to obtain a random int uses 6 bytes.
184+
const randomCache = new FastBuffer(6 * 1024);
185+
let randomCacheOffset = randomCache.length;
186+
let asyncCacheFillInProgress = false;
187+
const asyncCachePendingTasks = [];
188+
182189
// Generates an integer in [min, max) range where min is inclusive and max is
183190
// exclusive.
184191
function randomInt(min, max, callback) {
@@ -223,33 +230,58 @@ function randomInt(min, max, callback) {
223230
// than or equal to 0 and less than randLimit.
224231
const randLimit = RAND_MAX - (RAND_MAX % range);
225232

226-
if (isSync) {
227-
// Sync API
228-
while (true) {
229-
const x = randomBytes(6).readUIntBE(0, 6);
230-
if (x >= randLimit) {
231-
// Try again.
232-
continue;
233-
}
234-
return (x % range) + min;
233+
// If we don't have a callback, or if there is still data in the cache, we can
234+
// do this synchronously, which is super fast.
235+
while (isSync || (randomCacheOffset < randomCache.length)) {
236+
if (randomCacheOffset === randomCache.length) {
237+
// This might block the thread for a bit, but we are in sync mode.
238+
randomFillSync(randomCache);
239+
randomCacheOffset = 0;
240+
}
241+
242+
const x = randomCache.readUIntBE(randomCacheOffset, 6);
243+
randomCacheOffset += 6;
244+
245+
if (x < randLimit) {
246+
const n = (x % range) + min;
247+
if (isSync) return n;
248+
process.nextTick(callback, undefined, n);
249+
return;
235250
}
236-
} else {
237-
// Async API
238-
const pickAttempt = () => {
239-
randomBytes(6, (err, bytes) => {
240-
if (err) return callback(err);
241-
const x = bytes.readUIntBE(0, 6);
242-
if (x >= randLimit) {
243-
// Try again.
244-
return pickAttempt();
245-
}
246-
const n = (x % range) + min;
247-
callback(null, n);
248-
});
249-
};
250-
251-
pickAttempt();
252251
}
252+
253+
// At this point, we are in async mode with no data in the cache. We cannot
254+
// simply refill the cache, because another async call to randomInt might
255+
// already be doing that. Instead, queue this call for when the cache has
256+
// been refilled.
257+
asyncCachePendingTasks.push({ min, max, callback });
258+
asyncRefillRandomIntCache();
259+
}
260+
261+
function asyncRefillRandomIntCache() {
262+
if (asyncCacheFillInProgress)
263+
return;
264+
265+
asyncCacheFillInProgress = true;
266+
randomFill(randomCache, (err) => {
267+
asyncCacheFillInProgress = false;
268+
269+
const tasks = asyncCachePendingTasks;
270+
const errorReceiver = err && tasks.shift();
271+
if (!err)
272+
randomCacheOffset = 0;
273+
274+
// Restart all pending tasks. If an error occurred, we only notify a single
275+
// callback (errorReceiver) about it. This way, every async call to
276+
// randomInt has a chance of being successful, and it avoids complex
277+
// exception handling here.
278+
for (const { min, max, callback } of tasks.splice(0, tasks.length))
279+
randomInt(min, max, callback);
280+
281+
// This is the only call that might throw, and is therefore done at the end.
282+
if (errorReceiver)
283+
errorReceiver.callback(err);
284+
});
253285
}
254286

255287

0 commit comments

Comments
 (0)