Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: add topic - event loop, timers, nextTick() #4936

Merged
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
46f0702
doc: add topic - event loop, timers, `nextTick()`
techjeffharris Jan 28, 2016
5a28415
doc: add topic - event loop, timers, `nextTick()`
techjeffharris Jan 28, 2016
dc1b8a5
corrections suggested in the GitHub PR
techjeffharris Jan 29, 2016
bb5b682
Merge branch 'doc-topic-event-loop-timers-nextTick' of github.com:tec…
techjeffharris Jan 29, 2016
ba98380
removed file without .md extension
techjeffharris Jan 29, 2016
936bf17
add details to explanation of timers
techjeffharris Jan 29, 2016
35cf726
update to address comments on PR
techjeffharris Feb 17, 2016
f80d7cc
fixed typo, added example as per @trevnorris
techjeffharris Feb 24, 2016
254694b
fixed styling nits identified by @mscdex
techjeffharris Feb 24, 2016
45fb2fe
fixes suggested by @silverwind and @fishrock123
techjeffharris Feb 26, 2016
d6d76f5
addressed comments made on GH issue
techjeffharris Mar 25, 2016
c133caf
updated `setImmediate()` vs `setTimeout()` section
techjeffharris Mar 25, 2016
f425164
update overview, phase detail headings, wrap at 72
techjeffharris Mar 29, 2016
1bd3e6c
docs: minor nits on the libuv phases.
mcollina Mar 31, 2016
7574d4b
Removed second timer phase.
mcollina Mar 31, 2016
8dc6ecb
Merge pull request #1 from mcollina/doc-topic-event-loop-timers-nextTick
techjeffharris Mar 31, 2016
d82a7f1
fix nits presented by @ajafff
techjeffharris Mar 31, 2016
1dc26f6
fix backticks on line 205
techjeffharris Mar 31, 2016
82d0fb8
Improve wording `setTimeout()` vs `setImmediate()`
techjeffharris Apr 7, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
doc: add topic - event loop, timers, nextTick()
This is a rough draft of a [Topic](https://github.com/nodejs/docs/blob/master/GETTING-STARTED.md#what-we-write) that provides an
overview of the event loop, timers, and `process.nextTick()` that is
based upon a NodeSource "Need to Node" presentation hosted by
@trevnorris: [Event Scheduling and the Node.js Event Loop](https://nodesource.com/resources).
  • Loading branch information
techjeffharris committed Jan 29, 2016
commit 5a28415365a122ad990db3a3fdf81afac45313bd
174 changes: 174 additions & 0 deletions doc/topics/the-event-loop-timers-and-nexttick
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Overview of the Event Loop, Timers, and `process.nextTick()`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should end with a .md extension.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooops >_<


The Following diagram shows a simplified overview of the event loop's order of operations.

┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ pending callbacks │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────│ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
└──│ setImmediate │
└───────────────────────┘

note: each box will be referred to as a "phase" of the event loop.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe make this italic?


*There is a slight discrepancy between the Windows and the Unix/Linux
implementation, but that's not important for this demonstration. The most
important parts are here. There are actually seven or eight steps, but the
ones we care about--ones that Node actually uses are these four.*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this "what Node uses vs. what libuv provides?"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trevnorris will you clarify this, please? I'm not familiar with the relationship between Node.js and libuv enough to comment, myself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. There are other minor steps inbetween these four provided, but as I understand, they are not particularly notable to an end user. (i.e. how this interacts with the microtask queue.)

It is possible this should be expanded to encompass some of those, so long as it is still concise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libuv's event loop does have more steps than what is provided here. For example, uv_handle_t's are not cleaned up until the (near) final phase of the event loop uv__run_closing_handles(). node has no hooks into this phase, so I don't deem it note worthy.

Or that if the event loop is run with UV_RUN_ONCE (which node does) then there's an additional step of updating and running timers. This is also not notable because the first step of the event loop after looping around is to also process timers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also there's uv__run_idle() phase, but is only used in node to make sure uv__io_poll() doesn't block in case a setImmediate() has been scheduled. Is just implementation details that would only confuse the basic understanding of how the event loop works.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. There are other minor steps inbetween these four provided, but as I understand, they are not particularly notable to an end user. (i.e. how this interacts with the microtask queue.)

I'd lean towards documenting microtask queue behavior if it's feasible to do so here — I've definitely found that information useful in the past when dealing with builtin promises.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(But otherwise I'm totally on board with the simplification. We should note in the doc that libuv is providing the seven or eight steps, but that Node only exposes the steps described in this document to end users, primarily.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd lean towards documenting microtask queue behavior if it's feasible to do so here — I've definitely found that information useful in the past when dealing with builtin promises.

I think the most notable thing is that it's processed after the nextTick queue, but then after the microtask queue is processed the nextTick queue is processed again. Then back to the microtask queue. etc.

This is to handle the case that one queues the other. But might be sufficient to put that it's "processed at the same time as the nextTick queue".


## timers

This phase executes callbacks scheduled by `setTimeout()` and `setInterval()`.
When you create a timer, you make a call to setTimeout(). The event loop will
eventually enter the 'poll' phase which determines how many milliseconds remain
until the next timer. If there is a timer, it will wait for connections for that
many milliseconds. After that many milliseconds, it will break the 'poll' phase
and wrap back around to the timers phase where those callbacks can be processed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the poll phase of the event loop is entered the number of ms before the soonest timer is to be called is set as the poll's timeout. Meaning the poll phase will return after "timeout" ms.

Take note that the poll phase can only return while idle. Meaning execution of a callback is allowed to run to completion, and can cause unexpected delay running the timer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Updated to include these notes. Thanks for the further detail!


*Note: The 'poll' phase technically controls when timers are called due to its
ability to cause a thread to sit idly without burning CPU in order to stall the
event loop so the timer can execute.*

## pending callbacks:

This phase executes callbacks for specific types of TCP errors, for example.

## poll:

This is the phase in which the event loop sits and waits for incoming
connections to be received. Ideally, most scripts spend most of their time here.

## setImmediate:

`process.setImmediate()` is actually a special timer that runs in a separate
phase of the event loop. It uses a libuv API that schedules callbacks to execute
after the poll phase has completed.

Generally, as the code is executed, the event loop will eventually hit the
'poll' phase where it will wait for an incoming connection, request, etc.
However, after a callback has been scheduled with `setImmediate()`, at the start
of the poll phase, a check will be run to see if there are any callbacks
waiting. If there are none waiting, the poll phase will end and continue to the
`setImmediate` callback phase.

### setImmediate vs setTimeout

How quickly a `setImmediate()` callback is executed is only limited by how
quickly the event loop can be processed whereas a timer won't fire until the
number of milliseconds passed have elapsed.

The advantage to using `setImmediate()` over `setTimeout()` is that the lowest
value you may set a timer's delay to is 1 ms (0 is coerced to 1), which doesn't
seem like much time to us humans, but it's actually pretty slow compared to how
quickly `setImmediate()` can execute--the event loop operates on the microsecond
scale (1 ms = 1000 µs).

## nextTick:

### Understanding nextTick()

You may have noticed that `nextTick()` was not displayed in the diagram, even
though its a part of the asynchronous API. This is because nextTick is not
technically part of the event loop. Instead, it is executed at the end of each
phase of the event loop.

Looking back at our diagram, any time you call nextTick in a given phase, all
callbacks passed to nextTick will be resolved before the event loop continues.
This can create some bad situations because **it allows you to asynchronously
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might suggest nixing asynchronously here, since it's synchronously blocking asynchronous events.

"starve" your I/O by making recursive nextTick calls.** which prevents the
event loop from reaching the poll phase.

### Why would that be allowed?

Why would something like this be included in Node? Part of it is a design
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node.js

philosophy where an API should always be asynchronous even where it
doesn't have to be. Take this code snippet for example:

```javascript
function apiCall (arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(
callback,
new TypeError('argument should be a string'));
}
```

The snippet does an argument check and if its not correct, it will pass the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's

error to the callback. The API updated fairly recently to allow passing
arguments to nextTick allowing it to take any arguments passed after the callback
to be propagated as the arguments to the callback so you don't have to nest functions.

What we're doing is passing an error back to the user. As far as the event loop
is concerned, its happening **synchronously**, but as far as the user is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might consider splitting this sentence in two:

As far as the _event loop_ is concerned, this happens **synchronously**. However,
as far as the _user_ is concerned, it occurs **asynchronously**: `apiCall()` always
runs its callback *after* the rest of the user's code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Thank you!

concerned, it is happening **asynchronously** because the API of apiCall() was
written to always be asynchronous.

This philosophy can lead to some potentially problematic situations. Take this
snippet for example:

```javascript
// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall (callback) {
callback();
};

// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(function () {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should use ES2015 for our examples? IE fat arrow functions?

// since someAsyncApiCall has completed, bar hasn't been assigned any value
console.log('bar', bar); // undefined

});

var bar = 1;
```

The user defines `someAsyncApiCall()` to have an asynchronous signature,
actually operates synchronously. When it is called, the callback provided to
`someAsyncApiCall ()` is called in the same phase of the event loop
because `someAsyncApiCall()` doesn't actually do anything asynchronously. As a
result, the callback tries to reference `bar` but it may not have that variable
in scope yet because the script has not been able to run to completion.

By placing it in a nextTick, the script
still has the ability to run to completion, allowing all the variables,
functions, etc., to be initialized prior to the callback being called. It also
has the advantage of not allowing the event loop to continue. It may be useful
that the user be alerted to an error before the event loop is allowed to
continue.

## process.nextTick() vs setImmediate()

We have two calls that are similar as far as users are concerned, but their
names are confusing.

* nextTick fires immediately on the same phase
* setImmediate fires on the following iteration or 'tick' of the event loop

In essence, the names should be swapped. nextTick fires more immediately than
setImmediate but this is an artifact of the past which is unlikely to change.
Making this switch would break a large percentage of the packages on npm.
Every day more new modules are being added, which mean every day we wait, more
potential breakages occur. While they are confusing, the names themselves won't change.

*We recommend developers use setImmediate in all cases because its easier to
reason about.*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(And it leads to code that's compatible with a wider variety of environments, like browser JS.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically this can be achieved using Promise.resolve(1).then(...), which runs in similar fashion to the nextTickQueue.


## Two reasons to use nextTick:

1. Allow users to handle errors, cleanup any then unneeded resources, or
perhaps try the request again before the event loop continues.

2. If you were to run a function constructor that was to, say, inherit from
`EventEmitter` and it wanted to call an event within the constructor. You can't
emit an event from the constructor immediately because the script will not have
processed to the point where the user assigns a callback to that event. So,
within the constructor itself, you can set a callback to emit the event after
the constructor has finished, which provides the expected results.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't one want to use setImmediate in both of these cases?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Should use setImmediate in this case.
  2. We shouldnt use nexttick for sending events, instead it should be used for setting up listeners for events so that they always exist on the next tick of the event loop, the example you provided would be a good use case for setImmediate.

Maybe also an example as well would be awesome :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds like setImmediate should pretty much always be the choice...

Are there good reasons to use nextTick then? Or is it basically deprecated, but not really?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wrote up something about this: nodejs/docs#34 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chrisdickinson As for the constructor case, say the construction is happening in a setImmediate() and you don't emit until the next setImmediate(). That means an I/O related event could be triggered in the polling phase, thus causing events to fire in incorrect order.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This begs the questions:

  • does the event loop start once the input script has finished execution --OR--
  • does the event loop start then execute the input script, and if so, in which 'phase' is the input script executed? In the poll phase...?

If it is the former, then it seems to me that nextTick() would be the correct choice because it will be called once the input script has finished executing which prevents any possibility of I/O events being processed before your callback is executed, as described here: nodejs/docs#34 (comment)

If it is the latter and it IS in the poll phase, then it seems it wouldn't make much difference since the setImmediate phase is right after the poll phase.

From what I've gathered, I believe the former provides the most accurate analogy for this example.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@techjeffharris The event loop doesn't start until after bootstrap. Which includes the initial script passed to node. Here's a partial native stack of running node -e 'process.abort':

    frame #2: 0x00000000015e00d1 node_g`node::Abort(args=0x00007fffffffc160) + 17 at node.cc:1643
...
    frame #34: 0x0000000000ccfce1 node_g`v8::Function::Call(this=0x0000000001efe550, recv=(val_ = 0x0000000001efe330), argc=0, argv=0x0000000000000000) + 113 at api.cc:4422
    frame #35: 0x00000000015c7732 node_g`node::Environment::KickNextTick(this=0x0000000001ef2ae0) + 242 at env.cc:81
    frame #36: 0x00000000015d9f1c node_g`node::MakeCallback(env=0x0000000001ef2ae0, recv=(val_ = 0x0000000001efe330), callback=(val_ = 0x0000000001ef0ae8), argc=2, argv=0x00007fffffffd9d0) + 1916 at node.cc:1192
    frame #37: 0x00000000015da166 node_g`node::MakeCallback(env=0x0000000001ef2ae0, recv=(val_ = 0x0000000001efe330), symbol=(val_ = 0x0000000001ef0ad8), argc=2, argv=0x00007fffffffd9d0) + 214 at node.cc:1218
    frame #38: 0x00000000015da1e1 node_g`node::MakeCallback(env=0x0000000001ef2ae0, recv=(val_ = 0x0000000001efe330), method=0x00000000017f7aa6, argc=2, argv=0x00007fffffffd9d0) + 97 at node.cc:1228
    frame #39: 0x00000000015e359d node_g`node::EmitBeforeExit(env=0x0000000001ef2ae0) + 349 at node.cc:3893
    frame #40: 0x00000000015e40af node_g`node::StartNodeInstance(arg=0x00007fffffffdbb0) + 719 at node.cc:4088
    frame #41: 0x00000000015e3d56 node_g`node::Start(argc=1, argv=0x0000000001ea9ad0) + 262 at node.cc:4160
    frame #42: 0x0000000001618f72 node_g`main(argc=3, argv=0x00007fffffffdd18) + 34 at node_main.cc:44

If it had been in the event loop then you would have seen uv_run().

Even if you're in the poll phase then execution order of anything queued while in a setImmediate callback will be different from anything outside of it. It's always relevant to use nextTick in these cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent, thank you for clarifying that.