0

I had a look into the Blink codebase to answer this question about the maximum possible number of timers in JavaScript.

New timers are created by DOMTimerCoordinator::InstallNewTimeout(). It calls NextID() to retrieve an available integer key. Then, it inserts the new timer and the corresponding key into timers_.

int timeout_id = NextID();
timers_.insert(timeout_id, DOMTimer::Create(context, action, timeout,
                                            single_shot, timeout_id));

NextID() gets the next id in a circular sequence from 1 to 231-1:

int DOMTimerCoordinator::NextID() {
  while (true) {
    ++circular_sequential_id_;

    if (circular_sequential_id_ <= 0)
      circular_sequential_id_ = 1;

    if (!timers_.Contains(circular_sequential_id_))
      return circular_sequential_id_;
  }
}

What happen if all the IDs are in use?
What does prevent NextID() from entering in a endless loop?

The whole process is explained with more detail in my answer to that question.

David
  • 6,695
  • 3
  • 29
  • 46
  • Care to explain the downvote? I'd love to improve the question if possible. My C++ knowledge is limited and I'm really interested in understanding how Blink solutes the issue. – David Nov 03 '18 at 09:27
  • 1
    Out of curiosity, I copied _maximum possible number of timers in JavaScript_ to google and found [JavaScript Timer Congestion](http://fitzgeraldnick.com/2011/03/08/javascript-timer-congestion.html). This gave me the impression that it's indeed not intended to have that much timers that the IDs could be exhausted. Btw. for single processing and exhausted IDs `NextID()` wouldn't be blocking - it would dead-locking as it would endless spin and no other code to release a timer could be executed (not in this thread). When I wrote "blocking" I hat multi-threading in mind but I'm no JS expert. – Scheff's Cat Nov 03 '18 at 17:52
  • 1
    To assign 2^31 - 1 timeouts would probably take more time than the maximum possible timeout value. – Kaiido Nov 07 '18 at 00:59
  • @Kaiido - Very good point. But they could be all timers set by `setInterval()` because the list is common to both. In that case they would be around until the user clear them. – David Nov 07 '18 at 01:15

1 Answers1

2

I needed a bit to understand this but I believe I got it.

These are the steps which turned it into sense for me.

  1. circular_sequential_id_ is used as unique identifier. It's not exposed but from the other info I suspect it's an int with 32 bit (e.g. std::int32_t).

  2. I suspect circular_sequential_id_ is a member variable of class (or struct) DOMTimerCoordinator. Hence, after each call of NextID() it “remembers” the last returned value. When NextID() is entered circular_sequential_id_ is incremented first:

        ++circular_sequential_id_;
  3. The increment ++circular_sequential_id_; may sooner or later cause an overflow (Uuuh. If I remember right this is considered as Undefined Behavior but in real world it mostly simply wraps around.) and becomes negative. To handle this, the next line is good for:

        if (circular_sequential_id_ <= 0)
          circular_sequential_id_ = 1;
  4. The last statement in loop checks whether the generated ID is still in use in any timer:

        if (!timers_.Contains(circular_sequential_id_))
          return circular_sequential_id_;

    If not used the ID is returned. Otherwise, “Play it again, Sam.”

This brings me to the most reasonable answer:

Yes, this can become an endless loop...

...if 231 - 1 timers have been occupied and, hence, all IDs have been consumed.

  1. I assume with 231 - 1 timers you have much more essential other problems. (Alone, imaging the storage that those timers may need and the time to handle all of them...)

  2. Even if 231 - 1 timers are not a fatal problem, the function may cycle further until one of the timers releases it's ID and it can be occupied again. So, NextID() would be blocking if a resource (a free ID for a timer) is temporarily not available.

Thinking twice, the 2. option is rather theoretically. I cannot believe that somebody would manage limited resources this way.

I guess, this code works under assumption that there will never be 231 - 1 timers concurrently and hence it will find a free ID with a few iterations.

Scheff's Cat
  • 19,528
  • 6
  • 28
  • 56
  • @David Sorry, I just realized that you described it yourself in your other answer. So, please skip the upper part. The lower part might be the interesting for you. (And, OT, but: I started with a C64 but had some lessons from a related on an ZX Spectrum before...) ;-) – Scheff's Cat Nov 03 '18 at 07:53
  • 1
    +1. I agree with everything. I don't accept it right away in the hope of an expert on the Blink codebase dropping by and giving an authoritative answer. (OT - my best friend at that time got a C64; we were both so proud on our machines!) – David Nov 03 '18 at 09:23
  • And you remember right. A signed integer overflow is UB. Actually, Blink uses this kind of circular sequences in several places around the codebase, and some [have been rewritten in order to avoid UB](https://codereview.chromium.org/1338093003). – David Nov 03 '18 at 09:52
  • I may be mistaken but the documentation states that an ID will never repeat and you mention indirectly multiple times that an id can be freed (temporary unavailable, concurrent timers etc). Could you explain? – php_nub_qq Nov 04 '18 at 03:21
  • @php_nub_qq I don't know why the doc. does state this. (I even don't know this doc.) Are you sure that it really says "never repeat" or instead "never duplicated" or "unique". There will never be two timers with the same ID at the same time. Judging from the exposed code, I would say they can be repeated. On the other hand, 2^31 - 1 is a damn big number. ;-) Could you provide a link to that doc.? – Scheff's Cat Nov 04 '18 at 07:12
  • @php_nub_qq I had a look into the doc. provided by David: [timers](https://html.spec.whatwg.org/#timers). I didn't find such a statement of "never repeated" but it claims that: _have a list of active timers. Each entry in this lists is identified by a number, which must be unique within the list for the lifetime_. Please note, that there is also `clearTimeout()` and `clearInterval()`. If a timer is removed from the _list of active timers_, the above `NextID()` would recognize its ID as free if it's probed, because it just looks into this list. – Scheff's Cat Nov 04 '18 at 07:22
  • @Scheff [It is guaranteed that a timeout ID will never be reused by a subsequent call to setTimeout() or setInterval()](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Return_value), which I translate as "never repeated"? – php_nub_qq Nov 04 '18 at 07:41
  • @php_nub_qq Hmmm, agree: It sounds ambiguous somehow. Who do you trust more? Doc. text or source code? ;-) Though, again, 2^31 - 1 is a damn big number and each object has it's own ID pool. The probability that an ID repeats is rather small. I guess the browser will crash before. (That probability is much higher...) ;-) – Scheff's Cat Nov 04 '18 at 07:45
  • @php_nub_qq - Scheff has very good intuition. The oficial W3C specification states: "Each entry in these lists is identified by a number, which must be **unique within its list for the lifetime of the object**". Blink code meets the spec. – David Nov 04 '18 at 07:46
  • Ops, i'm writting from the phone, didn't see the last comments. The list for both kind of timers is common, you can, e.g., delete a timeout with `clearInterval()`. – David Nov 04 '18 at 07:55
  • 1
    I have [contacted Blink developers](https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/4o9_DKUL6mc) about `NextID()` but so far I have no answer. If they reply I will edit your answer here (hope that's OK with you). Thanks a lot for your help! – David Nov 07 '18 at 00:29