Skip to content

Commit 960c6be

Browse files
tniessenMylesBorins
authored andcommitted
crypto: add buffering to randomInt
PR-URL: #35110 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Andrey Pechkurov <apechkurov@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 0db1a1e commit 960c6be

File tree

2 files changed

+100
-25
lines changed

2 files changed

+100
-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

+62-25
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
const {
44
Array,
5+
ArrayPrototypeForEach,
6+
ArrayPrototypePush,
7+
ArrayPrototypeShift,
8+
ArrayPrototypeSplice,
59
BigInt,
610
FunctionPrototypeBind,
711
FunctionPrototypeCall,
@@ -186,6 +190,13 @@ function randomFill(buf, offset, size, callback) {
186190
// e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6);
187191
const RAND_MAX = 0xFFFF_FFFF_FFFF;
188192

193+
// Cache random data to use in randomInt. The cache size must be evenly
194+
// divisible by 6 because each attempt to obtain a random int uses 6 bytes.
195+
const randomCache = new FastBuffer(6 * 1024);
196+
let randomCacheOffset = randomCache.length;
197+
let asyncCacheFillInProgress = false;
198+
const asyncCachePendingTasks = [];
199+
189200
// Generates an integer in [min, max) range where min is inclusive and max is
190201
// exclusive.
191202
function randomInt(min, max, callback) {
@@ -230,33 +241,59 @@ function randomInt(min, max, callback) {
230241
// than or equal to 0 and less than randLimit.
231242
const randLimit = RAND_MAX - (RAND_MAX % range);
232243

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

262299

0 commit comments

Comments
 (0)