Search code examples
javascriptsettimeoutsetintervalcallstackevent-loop

Why don't I see task queue bunch up?


I am reading and conversing with chatGPT about what exactly setTimeout and setInterval do. As I understand it the main JavaScript execution thread sends the Web Timer API a callback function and an interval or set time, in ms, after which the callback function should be added to the task queue. After sending these two things to the the Web API the execution thread returns to the main call stack and continues executing the rest of the synchronous code. Now when the Web API receives the callback and the time, say its 1000ms, it starts a timer and at every 1000ms it sends the callback to the task queue. In the case of setTimeout when the Web API receives the callback and the time, say 5000ms, it starts a timer. After the this timer hits 5000ms the callback function gets added to the task queue. Now when these task queue functions are added to the call stack and get executed depends on how long the rest of the JavaScript code takes to all get executed so there is a potential for functions to have accumulated in the task queue before the call stack clears. This is what chatGPT calls "bunching up." And claims that if enough time is elapsed between the clearing of the call stack and the beginning of the execution of functions in the task queue you can have rapid successions of executions of the callbacks that don't adhere to the interval set in setInterval.

So I wrote a program to experiment:

function limitedRepeater() {
  return setInterval(() => console.log("hi for now"), 1000);
}

const someId = limitedRepeater();
setTimeout(() => clearInterval(someId), 5000);

This is pretty simple to understand. console.log("hi for now") calls are added to the task queue every 1000ms for 5000ms after the setTimeout timer gets initialized. This is the output and the time elapsed as computed by the windows PowerShell command Measure-Command:

Output:

hi for now

hi for now

hi for now

hi for now

Measure-Command {node .\asynchronous_js_frontendmasters.js}

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 5
Milliseconds      : 28
Ticks             : 50285701
TotalDays         : 5.82010428240741E-05
TotalHours        : 0.00139682502777778
TotalMinutes      : 0.0838095016666667
TotalSeconds      : 5.0285701
TotalMilliseconds : 5028.5701

Now what happens if I include a thread blocking piece of code after the setInterval function call but before the setTimeout call like so:

function limitedRepeater() {
  return setInterval(() => console.log("hi for now"), 1000);
}

const someId = limitedRepeater();
let sum = 0;
for (let i = 0; i < 90000000000; i++) {
  sum += i;
}
setTimeout(() => clearInterval(someId), 5000);

This for loop should create a significant block to the main execution thread. And it does:

Output:

hi for now

hi for now

hi for now

hi for now

hi for now

Measure-Command {node .\asynchronous_js_frontendmasters.js}

Days              : 0
Hours             : 0
Minutes           : 1
Seconds           : 7
Milliseconds      : 231
Ticks             : 672318270
TotalDays         : 0.000778146145833333
TotalHours        : 0.0186755075
TotalMinutes      : 1.12053045
TotalSeconds      : 67.231827
TotalMilliseconds : 67231.827

It adds more than a minute to the execution time! But only one more execution of console.log("hi for now"). chatGPT claims that the interval or timer passed to the Web API governs the time elapsed between additions of the callback to the task queue not necessarily time elapsed between executions by the call stack. But if this is the case shouldn't many more executions of console.log("hi for now") have been added to the task queue before the for loop and call to setTimeout? What am I missing? Thank you!


Solution

  • setInterval

    There is a serious misunderstanding of setInterval implementation in this quote from the post:

    Now when the Web API receives the callback and the time, say its 1000ms, it starts a timer and at every 1000ms it sends the callback to the task queue.

    Section 8.6 Timers of the HTML specification provides a list of "task initialization steps", comprising 13 steps in all, that browser must follow to implement timer API calls. Notes in green are explanatory, and only two steps need looking at:

    • Timer initialization Step 8 of the listed steps creates a task to put in the event loop's job queue when a timeout occurs. The task calls the handler supplied to the timer call when executed.

    • Step 8.4 is executed by the task when it runs. It requires the browser to run the complete set of initialization steps again if repeat is non zero, meaning it's part of a setInterval call.

      Effectively a new setTimeout call, using the id provided to the setInterval caller, is simulated after each and every interval timeout.


    The experiment

    1. setInterval is called with a timeout of 1000ms. This initially acts as setTimeout and sets up a system timer to queue a task to call the timer callback.

    2. Blocking code blocks the JavaScript thread for longer than 1000ms. This prevents the task created in (1) being executed. There is no build up of setInterval callbacks because only one task has been scheduled, and it can't be executed while the main thread is blocked.

    3. Blocking code setups up a timer for 5 seconds to clear the interval timer created in (1) and returns to the event loop.

    4. The task created in (1) can and is now executed. As part of its execution it creates console output and creates a new 1000ms timer with the same timer id.

    5. Within the next 5 seconds, interval timer implementation creates and runs 4 more tasks, creating 5 entries on the console in total.

    6. The timer task created in (3) runs and clears the interval timer.

    The experiment is a success!