@@ -179,6 +179,13 @@ function randomFill(buf, offset, size, callback) {
179179// e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6);
180180const 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.
184191function 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