diff --git a/lib/internal/process/next_tick.js b/lib/internal/process/next_tick.js index 84a7402117c5c2..0bced30445a170 100644 --- a/lib/internal/process/next_tick.js +++ b/lib/internal/process/next_tick.js @@ -32,10 +32,60 @@ function setupNextTick() { const kHasScheduled = 0; const kHasPromiseRejections = 1; - // Queue size for each tick array. Must be a factor of two. + // Queue size for each tick array. Must be a power of two. const kQueueSize = 2048; const kQueueMask = kQueueSize - 1; + // The next tick queue is implemented as a singly-linked list of fixed-size + // circular buffers. It looks something like this: + // + // head tail + // | | + // v v + // +-----------+ <-----\ +-----------+ <------\ +-----------+ + // | [null] | \----- | next | \------- | next | + // +-----------+ +-----------+ +-----------+ + // | tick | <-- bottom | tick | <-- bottom | [empty] | + // | tick | | tick | | [empty] | + // | tick | | tick | | [empty] | + // | tick | | tick | | [empty] | + // | tick | | tick | bottom --> | tick | + // | tick | | tick | | tick | + // | ... | | ... | | ... | + // | tick | | tick | | tick | + // | tick | | tick | | tick | + // | [empty] | <-- top | tick | | tick | + // | [empty] | | tick | | tick | + // | [empty] | | tick | | tick | + // +-----------+ +-----------+ <-- top top --> +-----------+ + // + // Or, if there is only one fixed-size queue, it looks something + // like either of these: + // + // head tail head tail + // | | | | + // v v v v + // +-----------+ +-----------+ + // | [null] | | [null] | + // +-----------+ +-----------+ + // | [empty] | | tick | + // | [empty] | | tick | + // | tick | <-- bottom top --> | [empty] | + // | tick | | [empty] | + // | [empty] | <-- top bottom --> | tick | + // | [empty] | | tick | + // +-----------+ +-----------+ + // + // Adding a value means moving `top` forward by one, removing means + // moving `bottom` forward by one. + // + // We let `bottom` and `top` wrap around, so when `top` is conceptually + // pointing to the end of the list, that means that the actual value is `0`. + // + // In particular, when `top === bottom`, this can mean *either* that the + // current queue is empty or that it is full. We can differentiate by + // checking whether an entry in the queue is empty (a.k.a. `=== undefined`). + class FixedQueue { constructor() { this.bottom = 0; @@ -50,11 +100,12 @@ function setupNextTick() { } shift() { - const next = this.list[this.bottom]; - if (next === undefined) return null; + const nextItem = this.list[this.bottom]; + if (nextItem === undefined) + return null; this.list[this.bottom] = undefined; this.bottom = (this.bottom + 1) & kQueueMask; - return next; + return nextItem; } } @@ -63,21 +114,34 @@ function setupNextTick() { function push(data) { if (head.bottom === head.top) { - if (head.list[head.top] !== undefined) + // Either empty or full: + if (head.list[head.top] !== undefined) { + // It's full: Creates a new queue, sets the old queue's `.next` to it, + // and sets it as the new main queue. head = head.next = new FixedQueue(); - else + } else { + // If the head is empty, that means that it was the only fixed-sized + // queue in existence. + DCHECK_EQ(head.next, null); + // This is the first tick object in existence, so we need to inform + // the C++ side that we do want to run `_tickCallback()`. tickInfo[kHasScheduled] = 1; + } } head.push(data); } function shift() { const next = tail.shift(); - if (tail.top === tail.bottom) { - if (tail.next) + if (tail.top === tail.bottom) { // -> .shift() emptied the current queue. + if (tail.next !== null) { + // If there is another queue, it forms the new tail. tail = tail.next; - else + } else { + // We've just run out of items. Let the native side know that it + // doesn't need to bother calling into JS to run the queue. tickInfo[kHasScheduled] = 0; + } } return next; }