Does JavaScript run out of timeout IDs?
Asked Answered
C

1

9

Surprisingly I can not find the answer to this question anywhere on the web.

In the documentation it is stated that setTimeout and setInterval share the same pool of ids, as well as that an id will never repeat. If that is the case then they must eventually run out because there is a maximum number the computer can handle? What happens then, you can't use timeouts anymore?

Caridadcarie answered 1/11, 2018 at 13:42 Comment(7)
The runtime does not impose a limit, but as with anything else there are ultimately finite resources on a given platform. I would imagine that a JavaScript system that might run out of timer slots would exhaust other resources long before that.Abiding
@Abiding not necessarily, consider a program which never stops and simply creates timeouts over and over after the previous one ends, or perhaps I don't see your point.Caridadcarie
So you envision a realistic system that creates millions and millions of timers?Abiding
@Abiding my particular concern arose from having to close a connection if no response is received after a ping in x amount of time. Pings are sent every 15 seconds and thus if I start a timeout for the connection termination which will get cleared when the response arrives, lots of timeouts will be created (not simultaneously) which would probably not but theoretically could exhaust the ids? It's less of a practical question, more like why this is not defined anywhere and what would actually happen.Caridadcarie
Yes, you definitely could exhaust the number if timer ids, but it would take a very long time. Do the math: MAX_SAFE_INTEGER is big enough for 285 thousand years worth of timers if you get a new id every millisecond.Abiding
@Abiding fair point. In my scenario (every 15 sec) I would need about 6 months for 1 million timers which is far from the limit.Caridadcarie
@pointy and that assumes that the ID is simply an integer. There's any number of other ways that it could be implemented (e.g. GUIDs), which wouldn't even have that ceiling. The specific implementation likely varies based on the JavaScript engine of the browser in question.Gneiss
S
10

TL;DR;

It depends on the browser's engine.

In Blink and Webkit:

  • The maximum number of concurrent timers is 231-1.
  • If you try to use more your browser will likely freeze due to a endless loop.

Official specification

From the W3C docs:

The setTimeout() method must run the following steps:

  1. Let handle be a user-agent-defined integer that is greater than zero that will identify the timeout to be set by this call.

  2. Add an entry to the list of active timeouts for handle.

  3. [...]

Also:

Each object that implements the WindowTimers interface has a list of active timeouts and a list of active intervals. Each entry in these lists is identified by a number, which must be unique within its list for the lifetime of the object that implements the WindowTimers interface.

Note: while the W3C mentions two lists, the WHATWG spec establishes that setTimeout and setInterval share a common list of active timers. That means that you can use clearInterval() to remove a timer created by setTimeout() and vice versa.

Basically, each user agent has freedom to implement the handle Id as they please, with the only requirement to be an integer unique for each object; you can get as many answers as browser implementations.

Let's see, for example, what Blink is doing.

Blink implementation

Previous note: It's not such an easy task to find the actual source code of Blink. It belongs to the Chromium codebase which is mirrored in GitHub. I will link GitHub (its current latest tag: 72.0.3598.1) because its better tools to navigate the code. Three years ago, they were pushing commits to chromium/blink/. Nowadays, active development is on chromium/third_party/WebKit but there is a discussion going on about a new migration.

In Blink (and in WebKit, which obviously has a very similar codebase), the responsible of maintaining the aforementioned list of active timers is the DOMTimerCoordinator belonging to each ExecutionContext.

// Maintains a set of DOMTimers for a given page or
// worker. DOMTimerCoordinator assigns IDs to timers; these IDs are
// the ones returned to web authors from setTimeout or setInterval. It
// also tracks recursive creation or iterative scheduling of timers,
// which is used as a signal for throttling repetitive timers.
class DOMTimerCoordinator {

The DOMTimerCoordinator stores the timers in the blink::HeapHashMap (alias TimeoutMap) collection timers_ which key is (meeting the specs) int type:

using TimeoutMap = HeapHashMap<int, Member<DOMTimer>>;
TimeoutMap timers_;

That answer your first question (in the contex of Blink): the maximum number of active timers for each context is 231-1; much lower than the JavaScript MAX_SAFE_INTEGER (253-1) that you mentioned but still more than enough for normal use cases.

For your second question, "What happens then, you can't use timeouts anymore?", I have so far just a partial answer.

New timers are created by DOMTimerCoordinator::InstallNewTimeout(). It calls the private member function NextID() to retrieve an available integer key and DOMTimer::Create for the actual creation of the timer object. 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_;
  }
}

It increments in 1 the value of circular_sequential_id_ or set it to 1 if it goes beyond the upper limit (although INT_MAX+1 invokes UB, most C implementations return INT_MIN).

So, when the DOMTimerCoordinator runs out of IDs, tries again from 1 up until it finds one free.

But, what happen if they are all in use? What does prevent NextID() from entering in a endless loop? It seems that nothing. Likely, Blink developers coded NextID() under the assumption that there will never be 231-1 timers concurrently. It makes sense; for every byte returned by DOMTimer::Create() you will need a GB of RAM to store timers_ if it is full. It can add up to TB if you store long callbacks. Let alone the time needed to create them.

Anyway, it looks surprising that no guard against an endless loop has been implemented, so I have contacted Blink developers, but so far I have no response. I will update my answer if they reply.

Sexy answered 3/11, 2018 at 1:53 Comment(2)
Related: https://mcmap.net/q/496306/-is-there-a-way-to-clear-all-javascript-timers-at-once (Is there a way to clear all JavaScript timers at once?...)Cheryllches
The OP was asking if the timer subsystem ever runs out of IDs if you sequentially start a timer and restart it each time it ends. They were not asking "how many timers can I start concurrently". So your statement So, when the DOMTimerCoordinator runs out of IDs, tries again from 1 up until it finds one free. answers the question: the ID generator simply starts over at "1", thus the timer ID pool never runs out if a single timer is executed sequentially. Can you confirm that we are understanding this is correctly?Seamus

© 2022 - 2024 — McMap. All rights reserved.