Skip to content

timers distribution predictability #15081

Closed

Description

@vsemozhetbyt
  • Version: 8.4.0
  • Platform: Windows 7 x64
  • Subsystem: libuv, promises

Revisiting this article, I've found out that an example which illustrates some known bug (?) in microtask queue execution behaves differently because of times of script call.

'use strict';

console.log('script start')

const interval = setInterval(() => {  
  console.log('setInterval')
}, 0)

setTimeout(() => {  
  console.log('setTimeout 1')
  Promise.resolve().then(() => {
    console.log('promise 3')
  }).then(() => {
    console.log('promise 4')
  }).then(() => {
    setTimeout(() => {
      console.log('setTimeout 2')          //< place of random microtask queue intrusion
      Promise.resolve().then(() => {
        console.log('promise 5')
      }).then(() => {
        console.log('promise 6')
      }).then(() => {
        clearInterval(interval)
      })
    }, 0)
  })
}, 0)

Promise.resolve().then(() => {  
  console.log('promise 1')
}).then(() => {
  console.log('promise 2')
})

Every first call in a new cmd.exe terminal produces the mentioned error. Every next call has proper execution order:

>node test.js
script start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
setInterval          //< intruder
promise 5
promise 6

>node test.js
script start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
promise 5
promise 6

Compare the next step after the setTimeout 2 output.

Activity

  1. changed the title [-]strange dependence between event loop and script times of script call[/-] [+]strange dependence between event loop and times of script call[/+] on Aug 30, 2017
  2. added
    libuvIssues and PRs related to the libuv dependency or the uv binding.
    promisesIssues and PRs related to ECMAScript promises.
    on Aug 30, 2017
  3. vsemozhetbyt commented on Aug 30, 2017

    @vsemozhetbyt
    ContributorAuthor

    The same is valid for v6.11.2, the last nightly and V8 canary.

  4. bzoz commented on Aug 30, 2017

    @bzoz
    Contributor

    On my box it is random, with each run the output will either have or have not this extra setInterval.

    The docs for setTimeout and setInterval say When delay is larger than 2147483647 or less than 1, the delay will be set to 1, so I guess that this is some kind of a race?

  5. vsemozhetbyt commented on Aug 30, 2017

    @vsemozhetbyt
    ContributorAuthor

    @bzoz I've checked some more times: the first time is always with extra setInterval, in the next ones — ~1 of 10 has extra setInterval.

  6. vsemozhetbyt commented on Aug 30, 2017

    @vsemozhetbyt
    ContributorAuthor

    Strangely, in git bash for Windows, the first time is also random, in the next ones statistics is reverted: ~1 of 10 has not extra setInterval.

  7. bzoz commented on Aug 30, 2017

    @bzoz
    Contributor

    On my box it is totally random, no matter if on cmd, bash or anything.

  8. mcollina commented on Aug 30, 2017

    @mcollina
    SponsorMember

    This is not a problem, but it depends on when setTimeout(func, 0) places func. This is explained in https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick.

  9. bzoz commented on Aug 30, 2017

    @bzoz
    Contributor

    The article says:
    According to the WHATVG specification, exactly one (macro)task should get processed from the macrotask queue in one cycle of the event loop
    while Nodes setImmediate documentation says:
    The entire callback queue is processed every event loop iteration.
    So I guess Node does not follow WHATVG exactly. More details are here.

    I think that the thing here is that in the article uses setTimeout(..., 0). Node makes this into setTimeout(..., 1). This 1 is a minimum delay, so it probably gets scheduled slightly differently each time.

  10. vsemozhetbyt commented on Aug 30, 2017

    @vsemozhetbyt
    ContributorAuthor

    This variant has the same behavior:

    'use strict';
    
    console.log('script start')
    
    const interval = setInterval(() => {
      console.log('setInterval')
    }, 0)
    
    setTimeout(() => {
      console.log('setTimeout 1')
      Promise.resolve().then(() => {
        console.log('promise 3')
      }).then(() => {
        console.log('promise 4')
      }).then(() => {
        setTimeout(() => {
          console.log('setTimeout 2')          //< place of random microtask queue intrusion
          process.nextTick(() => { console.log('nextTick 1'); })
          process.nextTick(() => { console.log('nextTick 2'); })
          process.nextTick(() => { clearInterval(interval); })
        }, 0)
      })
    }, 0)
    
    Promise.resolve().then(() => {
      console.log('promise 1')
    }).then(() => {
      console.log('promise 2')
    })
    >node test.js
    script start
    promise 1
    promise 2
    setInterval
    setTimeout 1
    promise 3
    promise 4
    setInterval
    setTimeout 2
    setInterval          //< intruder
    nextTick 1
    nextTick 2
    
    >node test.js
    script start
    promise 1
    promise 2
    setInterval
    setTimeout 1
    promise 3
    promise 4
    setInterval
    setTimeout 2
    nextTick 1
    nextTick 2
  11. benjamingr commented on Aug 30, 2017

    @benjamingr
    Member

    Very nice find!

    Just pointing out that the whole micro/macrotask semantics aren't really specified anywhere - and we're free to schedule promises (as a platform as we choose).

    We currently schedule them with microtask semantics (again, this is not required by the specification but is more correct since it does not defer things by I/O).

    I'd like to point out that the order of callbacks will likely change in the future as a part of nodejs/promises#31 since we currently needlessly defer a lot of actions to the nextTick where we don't have to which I hope V8 will fix.

    Have you tested what happens when you replace all the Promise.resolve().then(() => ... calls with nextTick calls?

  12. vsemozhetbyt commented on Aug 30, 2017

    @vsemozhetbyt
    ContributorAuthor

    @benjamingr If I get this right, this is the equivalent:

    'use strict';
    
    console.log('script start');
    
    const interval = setInterval(() => { console.log('setInterval'); }, 0);
    
    setTimeout(() => {
      console.log('setTimeout 1');
      process.nextTick(() => { console.log('nextTick 3'); });
      process.nextTick(() => { console.log('nextTick 4'); });
      process.nextTick(() => {
        setTimeout(() => {
          console.log('setTimeout 2');          //< place of random microtask queue intrusion
          process.nextTick(() => { console.log('nextTick 5'); });
          process.nextTick(() => { console.log('nextTick 6'); });
          process.nextTick(() => { clearInterval(interval); });
        }, 0);
      });
    }, 0);
    
    process.nextTick(() => { console.log('nextTick 1'); });
    process.nextTick(() => { console.log('nextTick 2'); });

    It is more random for me (even the first run in the new terminal has random output), but the extra setInterval emerges inside microtask queue more seldom:

    > node test.js
    script start
    nextTick 1
    nextTick 2
    setInterval
    setTimeout 1
    nextTick 3
    nextTick 4
    setInterval
    setTimeout 2
    setInterval          //< intruder
    nextTick 5
    nextTick 6
    
    > node test.js
    script start
    nextTick 1
    nextTick 2
    setInterval
    setTimeout 1
    nextTick 3
    nextTick 4
    setInterval
    setTimeout 2
    nextTick 5
    nextTick 6
  13. benjamingr commented on Aug 30, 2017

    @benjamingr
    Member

    @vsemozhetbyt good, that looks a lot more manageable than if the problem was in the promise glue code but not in the nextTick queue.

    Removing the libuv and promises labels and adding the timers label. Also pinging @mscdex about timers :)

  14. added
    timersIssues and PRs related to timers, setImmediate(), setInterval(), and setTimeout().
    on Aug 30, 2017
  15. 17 remaining items

  16. benjamingr commented on Aug 30, 2017

    @benjamingr
    Member

    For example, Bluebird an Q which are the two most popular promise libraries schedule promises with macrotask semantics (with setImmediate) and that has never been a problem although we considered changing it many times. The issue just never rose.

  17. vkurchatkin commented on Aug 30, 2017

    @vkurchatkin
    Contributor

    @benjamingr the equivalent of nextTick is microtask queue (job queue).

  18. benjamingr commented on Aug 30, 2017

    @benjamingr
    Member

    @vkurchatkin right, but Bluebird schedules with setImmediate and macrotask semantics explicitly. (It runs all scheduled tasks together though, rather than schedule them individually)

  19. vkurchatkin commented on Aug 30, 2017

    @vkurchatkin
    Contributor

    @benjamingr well, that's one reason to avoid Bluebird.

  20. benjamingr commented on Aug 30, 2017

    @benjamingr
    Member

    @vkurchatkin I don't think it is, but if you have a compelling argument on why nextTick semantics is inherently better I'm all ears. Node that bluebird typically doesn't schedule at all - only when the promise itself resolved synchronously.

    There are plenty of reasons to not pick bluebird and hopefully we will reach performance parity with native promises but scheduling on macrotick semantics worked so well (well enough) that the two most popular libraries with over 25M monthly downloads on NPM do it and (almost) nobody noticed.

  21. vsemozhetbyt commented on Aug 30, 2017

    @vsemozhetbyt
    ContributorAuthor

    Is this answer, referenced in some place, applicable for Node.js?

    In both cases, this output fragment still seems strange:

    setInterval
    setTimeout 2
    setInterval 
    promise 5
    promise 6
    

    How can we parse it if the answer is true?

    --------------------event loop go-around 1
    setInterval
    --------------------event loop go-around 2
    setTimeout 2
    --------------------event loop go-around 3
    setInterval 
    promise 5
    promise 6

    This seems not OK: in the event loop go-around 3 we have microtask queue from the event loop go-around 2.
    If the answer is not true, what would be the separation?

    --------------------event loop go-around 1
    setInterval
    setTimeout 2
    --------------------event loop go-around 2
    setInterval 
    promise 5
    promise 6

    We still have microtask queue from the previous loop go-around.

    --------------------event loop go-around 1
    setInterval
    --------------------event loop go-around 2
    setTimeout 2
    setInterval 
    promise 5
    promise 6

    We have one missing macrotask in the event loop go-around 1 (and reverted order of handler execution in event loop go-around 2? — or setTimeout/setInterval order in the next setInterval runs is random by design? ).

    --------------------event loop go-around 1
    setInterval
    setTimeout 2
    setInterval 
    promise 5
    promise 6

    We have two identical macrotasks in one event loop go-around

  22. benjamingr commented on Aug 30, 2017

    @benjamingr
    Member

    We should probably document that the ordering of asynchronous actions is not guaranteed by Node.js.

  23. vsemozhetbyt commented on Aug 31, 2017

    @vsemozhetbyt
    ContributorAuthor

    I am trying to visually separate event loop go-around with setImmediate() calls (I am not sure if this is a right approach though). So this is a simplified version of the above-used cases:

    'use strict';
    
    const outputs = [];
    
    outputs.push('script start');
    
    const interval = setInterval(() => {
      outputs.push('setInterval');
      setImmediate(() => { outputs.push('-------------------- setImmediate'); });
    }, 0);
    
    setTimeout(() => {
      outputs.push('setTimeout 1');
      setTimeout(() => {
        outputs.push('setTimeout 2');
        clearInterval(interval);
        setImmediate(() => { console.log(outputs.join('\n')); });
      }, 0);
    }, 0);
    
    outputs.push('-------------------- Script ends');

    It randomly outputs these 2 variants:

    script start
    -------------------- Script ends
    setInterval
    setTimeout 1
    -------------------- setImmediate
    setInterval
    setTimeout 2
    -------------------- setImmediate
    script start
    -------------------- Script ends
    setInterval
    setTimeout 1
    setInterval
    -------------------- setImmediate
    -------------------- setImmediate
    setTimeout 2
    1. The first one seems valid if we admit that these statements are not applicable to Node.js:

    One go-around of the event loop will have exactly one task being processed from the macrotask queue (this queue is simply called the task queue in the WHATWG specification).

    According to the WHATVG specification, exactly one (macro)task should get processed from the macrotask queue in one cycle of the event loop.

    but this is applicable:

    when the event loop enters a given phase, it will perform any operations specific to that phase, then execute callbacks in that phase's queue until the queue has been exhausted or the maximum number of callbacks has executed.

    1. I still cannot understand how the second variant can be parsed and substantiated.
  24. vkurchatkin commented on Aug 31, 2017

    @vkurchatkin
    Contributor

    @vsemozhetbyt well, node doesn't have macrotasks, so it's definitely not applicable.

  25. benjamingr commented on Aug 31, 2017

    @benjamingr
    Member

    We should probably document that the ordering of asynchronous actions is not guaranteed by Node.js.

    Is this something you agree with in general - or do you think the current way is fine?

    (I'm +0, I think Node.js has traditionally been doing a good job in communicating that this is unspecified)

  26. vsemozhetbyt commented on Aug 31, 2017

    @vsemozhetbyt
    ContributorAuthor

    Well, I've read some articles (and watched some videos) about event loop with all these schemes of event loop phases and careful statements about execution in order of queueing — and still did not know that we had so unpredictable distribution of queued tasks between event loop go-arounds(

  27. vkurchatkin commented on Aug 31, 2017

    @vkurchatkin
    Contributor

    did not know that we had so unpredictable distribution of queued tasks

    Well, timers are uninvolved, so it shouldn't be surprising.

  28. vsemozhetbyt commented on Sep 4, 2017

    @vsemozhetbyt
    ContributorAuthor

    Closure.

    With asynk_hooks, I can see that there is no guarantee of timers distribution between loop runs.

    With this code:
    const async_hooks = require('async_hooks');
    const { writeSync } = require('fs');
    
    const types = {};
    let indent = 0;
    function syncLog(msg) { writeSync(1, `${msg}\n`); }
    
    const hooks = {
      init(asyncId, type) {
        types[asyncId] = type;
        syncLog(`${' '.repeat(indent * 2)}${asyncId} ^ (${type})`);
      },
      before(asyncId) {
        syncLog(`${' '.repeat(indent * 2)}${asyncId} > (${types[asyncId]})`);
        indent++;
      },
      after(asyncId) {
        indent--;
        syncLog(`${' '.repeat(indent * 2)}${asyncId} < (${types[asyncId]})`);
      },
      destroy(asyncId) {
        syncLog(`${' '.repeat(indent * 2)}${asyncId} x (${types[asyncId]})`);
      },
    };
    
    async_hooks.createHook(hooks).enable();
    
    const interval = setInterval(() => {
      setImmediate(() => {});
    }, 0);
    
    setTimeout(() => {
      setTimeout(() => {
        clearInterval(interval);
      }, 0);
    }, 0);

    I get at least these variants (brackets mean TIMERWRAP group, semicolons separate loop phases):

    1 (setInterval) + (setTimeout); setImmediate; (setInterval + setTimeout); setImmediate

    Output:
    2 ^ (Timeout)
    3 ^ (TIMERWRAP)
    4 ^ (Timeout)
    3 > (TIMERWRAP)
      2 > (Timeout)
        5 ^ (Immediate)
      2 < (Timeout)
    3 < (TIMERWRAP)
    3 > (TIMERWRAP)
      4 > (Timeout)
        6 ^ (Timeout)
      4 < (Timeout)
    3 < (TIMERWRAP)
    4 x (Timeout)
    5 > (Immediate)
    5 < (Immediate)
    5 x (Immediate)
    3 > (TIMERWRAP)
      2 > (Timeout)
        7 ^ (Immediate)
      2 < (Timeout)
      6 > (Timeout)
      6 < (Timeout)
    3 < (TIMERWRAP)
    2 x (Timeout)
    6 x (Timeout)
    7 > (Immediate)
    7 < (Immediate)
    7 x (Immediate)
    3 x (TIMERWRAP)

    2.1 (setInterval + setTimeout) + (setInterval + setTimeout); setImmediate + setImmediate

    Output:
    2 ^ (Timeout)
    3 ^ (TIMERWRAP)
    4 ^ (Timeout)
    3 > (TIMERWRAP)
      2 > (Timeout)
        5 ^ (Immediate)
      2 < (Timeout)
      4 > (Timeout)
        6 ^ (Timeout)
      4 < (Timeout)
    3 < (TIMERWRAP)
    4 x (Timeout)
    3 > (TIMERWRAP)
      2 > (Timeout)
        7 ^ (Immediate)
      2 < (Timeout)
      6 > (Timeout)
      6 < (Timeout)
    3 < (TIMERWRAP)
    2 x (Timeout)
    6 x (Timeout)
    5 > (Immediate)
    5 < (Immediate)
    7 > (Immediate)
    7 < (Immediate)
    5 x (Immediate)
    7 x (Immediate)
    3 x (TIMERWRAP)

    2.2 (setInterval) + (setTimeout + setInterval) + (setTimeout); setImmediate + setImmediate

    Output:
    2 ^ (Timeout)
    3 ^ (TIMERWRAP)
    4 ^ (Timeout)
    3 > (TIMERWRAP)
      2 > (Timeout)
        5 ^ (Immediate)
      2 < (Timeout)
    3 < (TIMERWRAP)
    3 > (TIMERWRAP)
      4 > (Timeout)
        6 ^ (Timeout)
      4 < (Timeout)
      2 > (Timeout)
        7 ^ (Immediate)
      2 < (Timeout)
    3 < (TIMERWRAP)
    4 x (Timeout)
    3 > (TIMERWRAP)
      6 > (Timeout)
      6 < (Timeout)
    3 < (TIMERWRAP)
    2 x (Timeout)
    6 x (Timeout)
    5 > (Immediate)
    5 < (Immediate)
    7 > (Immediate)
    7 < (Immediate)
    5 x (Immediate)
    7 x (Immediate)
    3 x (TIMERWRAP)

    If this is true, it seems there is no unexpectedness in the original article example output or next outputs in this issue. So I can close this issue for now. Feel free to reopen if needed or to add any useful information.

  29. changed the title [-]setInterval randomly intrudes in microtask queue[/-] [+]timers distribution predictability[/+] on Sep 4, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

promisesIssues and PRs related to ECMAScript promises.timersIssues and PRs related to timers, setImmediate(), setInterval(), and setTimeout().

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions