Quantcast
Viewing latest article 2
Browse Latest Browse All 3

On the nature of timers

UPDATE: I’m conducting a survey on JS timers that I’d ask if you would please consider taking before reading this article. Thanks!


What kind of timers? JavaScript’s setTimeout(..) and setInterval(..), specifically.

Actually, they don’t belong to the JS engine, but to the hosting environment (browser, Node.js, etc) that the JS engine is running in. Timers are exposed to your JS code via these interfaces, but the timers are actually not JS themselves.

Anyway, what I’m going to explore here is the existing state of timers — how unstable and unpredictable they’ve been all along — and what if anything we could change about them.

Let me clear up some potential confusion before we go any further. We could focus on any combination of these questions; you need to know which one(s) I am exploring, to understand what is and isn’t in scope for discussion:

  1. When I execute setTimeout( fn, 1000 );, does fn() fire at 1000ms from this moment, within timer precision/rounding and event loop contention?
  2. When I execute setTimeout( fn1, 1000 ); setTimeout( fn2, 1000 );, are both timers created at the same time (within the same millisecond), such that both 1000s start counting from the same timestamp? Should they be?
  3. When I execute setTimeout( fn1, 1000 ) and then 5ms later execute setTimeout( fn2, 995 ) (so that fn1() and fn2() should happen “at the same time”), is there any predictability that fn2() runs before or after fn1(), reliably? Should there be?

To boil it down simply: point (3) is the main concern driving this blog post, and as part of exploring that question, (2) comes into the mix. (1), the one most people probably think I’m talking about, is not actually my concern.

Timer Objections

Before I explore (3) (and (2)), let me address the objection(s) that I know may be incumbent on the mindset of any reader.

“Timers are inprecise and shouldn’t ever be relied upon.”

Yeah. But, this is really just an objection to (1), which I already said is not what I’m concerned with. Moreover, this assertion is more confirmed by observation of how unreliable they’ve always been, than a driving design force for the timers features.

If timers were actually designed to be fundamentally unreliable (aka imprecise), then the design of the API wouldn’t have given you the option to pick specific numbers for the interval. You could imagine an API like doLater( fn ) or waitUntilIdle( fn ) if that was the concern.

I do not personally care that much that a timer doesn’t fire at the exact, precise timestamp suggested. I think that’s an unreasonable expectation of a timer. setTimeout( fn, 953 ) to me says, “I want to wait approximately and at least 953ms before trying to run fn().”

If the function runs after 945ms, this is a failure. If the function runs at or shortly after 953ms, this is a success. If it runs significantly after 953ms, that’s OK, but there probably needed to be a good reason, like the entire thread being blocked unexpectedly.

Timer Expectations

OK, so dispensing with those objections, let’s turn our attention to (3). Actually, I’m going to simplify the case a bit to start:

setTimeout( fn1, 1000 );
setTimeout( fn2, 1000 );

Do you think this code snippet has any implication that fn1() will run before fn2() runs? I don’t mean strictly before, like with nothing in between, but I mean the relative ordering being predictably and reliably fn1() then fn2()? I’ve done some informal polling on this, and so far, almost everyone has said yes, they expect that.

The few dissents I’ve had have basically just jadedly said, “nothing’s predictable with timers”. OK, that may be our current reality, but I’m exploring whether that should be our current reality.

I strongly feel that code makes a promise — the expectation it sets, not the actual ES6 future value container thingy — of implied ordering.

The motivation is not that fn2() relies on fn1() finishing… that would be an actual async flow control concern, more suitably answered by ES6 Promises, for example.

I’m allowing for the fact that fn1() may run, and then some other stuff may happen, and then fn2() would run, and that ostensibly these two actions are independent, so creating a flow control relationship between them is inappropriate at worst, overkill at best.

What I’m asserting, pseudo-philosophically, is that we can observe relative ordering of two actions, and even expect such, without there being any explicit relationship between the two. It’s kinda like the difference between correlation and causation.

Even without knowing exactly what the browser does under the covers, I think it’s a reasonable interpretation/expectation that the code implies that fn1() should run before fn2() because the two of them end up in a single-file line and the first one registered should get there first.

The fact that it’s at least possible (either by under-specification, browser bug, or whatever) for the ordering to end up as fn2() > fn1(), because of implementation detail nuances far beyond what I’m going to discuss right now, is in my opinion evidence of a broken mechanism that needs fixing.

Let me illustrate further:

setTimeout( fn1, 1000 );
setTimeout( fn2, 999 );

Now what do you think? Do you think this code should imply/ensure that fn2() runs before fn1()? I mean, from an abstract level, it sure seems that way. fn2() appears to wait less time to be scheduled than fn1(), so no matter how long it takes them to run, fn2() should go before fn1(), right?

While researching this topic, I discovered a potential bug in Chrome Canary that suggests this is not always a strict guarantee. Even if you test it right now in your browser of choice, and it works “as expected” several times in a row, just how sure are you that this is in fact guaranteed by the timers mechanism?

There seems to be a fair amount of disagreement here. I found some who quoted me parts of the spec that suggest it’s supposed to work as I’m asserting, and others who parrot back the, “well timers gonna be crazy timers” party line.

Other Factors

Perhaps an eagle eye among you readers spotted a potential other explanation for the observed problem.

If the first setTimeout(..) call takes 1ms or more, than by the time the second setTimeout(..) runs, even though it says 999, that 999 is relative to a different, later timestamp than the 1000 was relative to, and it’s entirely possible that it’s accurate for fn1() to run first.

Or… is it?

Here’s another philosophical question for you to ponder (which evokes (2) from earlier): should all timers set up in a particular tick of the event loop be relative to the same timestamp (from the beginning of the event loop, for instance)?

If that were true, it wouldn’t matter how long running any of your code is, the 999 and 1000 would both be relative to the same timestamp, and should thus end up in a predictable running order.

Side Note: I see the difference between 1000 and 999 as important only relative to each other, in a similar way to how CSS z-index values really just imply relative stacking order, since any other element can trump your top-level stacking by picking a bigger number; all you can really say is that a 1000 is higher than a 999.

The problem with the timestamps being per timer rather than per event loop is that it is entirely impossible for a developer to predict just how long a piece of code will take to run, nor should any JS developer be reasoning about the execution durations of individual statements. That’s some kind of crazy premature optimization at best.

So:

setTimeout( fn1, 1000 );

someActionThatMayTakeApprox5milliseconds();

setTimeout( fn2, 995 );

It is impossible to reason about this program in a way that gives you predictable results.

That is, unless of course both setTimeout(..)s are relative to the same timestamp regardless of that action in between them. If they are relative to the beginning of the event loop tick and not the duration of, or when they occur in, the tick’s processing, then it’s perfectly reasonable to expect that fn2() will run before fn1().

Back to the earlier question… is setTimeout(..) possibly taking longer than 1ms to run? Actually, modern browsers have a high precision, highly efficient mechanism for testing such things: performance.now().

Consider:

var ts1, ts2, ts3;

ts1 = performance.now();
setTimeout( fn1, 1000 );
ts2 = performance.now();
setTimeout( fn2, 999 );
ts3 = performance.now();

console.log( ts1, ts2, ts3 );

If you try that, you’ll see something like:

4264.860000000001 4264.895 4264.910000000001

These numbers are the number of ms (with decimal fractions out to much higher precision) since the page started. If you run this code in an empty tab, and run it soon after the load, you’ll get a low number, like I did, of like 4264, which is 4264ms since page load. Each refresh of the page starts over at 0.

But that’s not important. What’s important is the difference between those 3 numbers, which as you can see is ~0.035ms (35 microseconds) and ~0.015ms (15 microseconds). That’s ridiculously fast. That’s just not even remotely accounting for the differences observed in ordering.

But then again, if we did in fact do some non-trivial work and take up a few milliseconds between the first and second setTimeout(..) calls, does it really make sense for those to have such unpredictable ordering?

Stable Timers

Let me propose how I think timers should work, and give this description a label, “stable timers”:

  1. If two or more timer callbacks, for whatever reason (timer precision, rounding, thread contention delays, etc), are going to fire “at the same time” (scheduled to start within the same millisecond), they should strictly be ordered first by their expressed interval in ascending order (999 before 1000), and then if need-be, by the order of setTimeout(..) calls, which can be implied by the incrementing ID number.
  2. If two or more setTimeout(..) and/or setInterval(..) calls are executed in the same event loop tick, their expressed intervals should be relative to a single shared timestamp (millisecond precision) captured at the beginning of the event loop tick, so that no matter where they appear in the code, or in what order, their timings will be predictably relative.

I am not, in any way/shape/form, suggesting that timers have to be changed/constrained to have accuracy to an implied moment in time, or that timer precision or rounding needs to change.

I’m only proposing that multiple timers should have predictable, reliable, non-racy relative ordering semantics with each other (as if all placed in one sorted queue). setTimeout(..999) should always mean something different in terms of observable outcome than setTimeout(..1000), because the developer definitely meant something different when making those two calls.

Global Breakage

But, you say, “Changing such an old, established, widespread-used mechanism would wreak havoc on the web, breaking sites all over!! ZOMG!”

Really?

Since the current status is that timers are not predictable/reliable/stable, if we made them a little more predictable/reliable/stable, how could that possibly break anything? You mean to suggest that there are sites which are relying on inconsistency? Nonsense.

Besides, browsers futz around with timers all the time, changing the precision/rounding from 16ms to 4ms to 1ms and back to 15ms. You don’t think that changes things on the web? Of course it does. Does it “break” stuff? Well, it may make things slower or faster or more or less racy.

But as everyone is so fond of reminding me, we shouldn’t have been relying on any of that anyway. What I’m proposing would actually bring timers to a state where, at least in some sense, we could start relying on them more than we do. How could that not be a good thing for the web?

Clear Timeout

I’ve started a project called stable-timers for expressly this purpose. At the time of this writing, it replaces the built-in timers with methods that have (some of) the stable behavior described above. This project is brand-new alpha, so it’s a rough work in progress. I don’t have all of that stability fully implemented yet. Want to help?


Viewing latest article 2
Browse Latest Browse All 3

Trending Articles