|
2 | 2 |
|
3 | 3 | const {
|
4 | 4 | Array,
|
| 5 | + ArrayPrototypeForEach, |
| 6 | + ArrayPrototypePush, |
| 7 | + ArrayPrototypeShift, |
| 8 | + ArrayPrototypeSplice, |
5 | 9 | BigInt,
|
6 | 10 | FunctionPrototypeBind,
|
7 | 11 | FunctionPrototypeCall,
|
@@ -186,6 +190,13 @@ function randomFill(buf, offset, size, callback) {
|
186 | 190 | // e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6);
|
187 | 191 | const RAND_MAX = 0xFFFF_FFFF_FFFF;
|
188 | 192 |
|
| 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 | + |
189 | 200 | // Generates an integer in [min, max) range where min is inclusive and max is
|
190 | 201 | // exclusive.
|
191 | 202 | function randomInt(min, max, callback) {
|
@@ -230,33 +241,59 @@ function randomInt(min, max, callback) {
|
230 | 241 | // than or equal to 0 and less than randLimit.
|
231 | 242 | const randLimit = RAND_MAX - (RAND_MAX % range);
|
232 | 243 |
|
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; |
242 | 261 | }
|
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(); |
259 | 262 | }
|
| 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 | + }); |
260 | 297 | }
|
261 | 298 |
|
262 | 299 |
|
|
0 commit comments