@@ -179,6 +179,13 @@ function randomFill(buf, offset, size, callback) {
179
179
// e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6);
180
180
const RAND_MAX = 0xFFFF_FFFF_FFFF ;
181
181
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
+
182
189
// Generates an integer in [min, max) range where min is inclusive and max is
183
190
// exclusive.
184
191
function randomInt ( min , max , callback ) {
@@ -223,33 +230,58 @@ function randomInt(min, max, callback) {
223
230
// than or equal to 0 and less than randLimit.
224
231
const randLimit = RAND_MAX - ( RAND_MAX % range ) ;
225
232
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 ;
235
250
}
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 ( ) ;
252
251
}
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
+ } ) ;
253
285
}
254
286
255
287
0 commit comments