Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't this setTimeout-based code work in Firefox with a small timeout (works in Internet Explorer/Chrome)?

I have the following code which demonstrates the difference in calling a long-running function directly from an event trigger, vs. using setTimeout().

Intended behavior:

  • When the first button is pressed, it appears pressed, the calculation runs for several seconds, then when the calculation finishes, the button appears depressed again and the second column changes from "not calculating yet" to "calculation done". (I won't elaborate on why that is supposed to happen; it's explained in linked answer.)

  • When the second button is pressed, the button depresses immediately; the second column immediately changes to "calculating..." text. When the calculation finishes several seconds later, the second column changes from "calculating..." to "calculation done".

What actually happens:

  • This works perfectly in Chrome (both buttons behave as expected)

  • This works perfectly in Internet Explorer 8

  • This does NOT work in Firefox (v.25) as-is. Specifically, the second button behaves 100% as the first one.

    • Changing the timeout in setTimeout() from 0 to 1 has no effect

    • Changing the timeout in setTimeout() from 0 to 500 works

Which leaves me with a big conundrum.

According to the whole reason behind why setTimeout() works whereas lack of one doesn't, the delay should have zero effect on how things work, since setTimeout()'s main purpose is to change the queuing order here, NOT to delay things.

So, why is it not working with delay 0 or 1 on Firefox, but works as expected with delay 500 (and works with any delay on Internet Explorer 8/Chrome)?

UPDATE: In addition to source code below, I also made a JSFiddle. But for some reason JSFiddle refuses to even load on my Internet Explorer 8, so for that testing, the code below is required.

UPDATE2: Someone raised the possibility of there being an issue with configuration setting dom.min_timeout_value in Firefox. I have edited it from 4 to 0, restarted the browser, and nothing was fixed. It still fails with a timeout of 0 or 1 and succeeds with 500.


Here is my source code - I simply saved it to a HTML file on C: drive and opened in all three browsers:

<html><body>
<script src="http://code.jquery.com/jquery-1.9.1.js"></script>

<table border=1>
    <tr><td><button id='do'>Do long calc - bad status!</button></td>
        <td><div id='status'>Not Calculating yet.</div></td></tr>
    <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
        <td><div id='status_ok'>Not Calculating yet.</div></td></tr>
</table>

<script>
function long_running(status_div) {
    var result = 0;
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 200; k++) {
                result = result + i + j + k;
            }
        }
    }
    $(status_div).text('calclation done');
}

// Assign events to buttons
$('#do').on('click', function () {
    $('#status').text('calculating....');
    long_running('#status');
});
$('#do_ok').on('click', function () {
    $('#status_ok').text('calculating....');
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
});
</script>
</body></html>

To test, you will need to change the nested loop boundaries to 300/100/100 for Internet Explorer 8; or to 1000/1000/500 for Chrome, due to different sensitivity of "this JS is taking too long" error coupled with JavaScript engine speed.

like image 540
DVK Avatar asked Dec 23 '13 16:12

DVK


People also ask

Is there a limit to setTimeout?

Browsers including Internet Explorer, Chrome, Safari, and Firefox store the delay as a 32-bit signed integer internally. This causes an integer overflow when using delays larger than 2,147,483,647 ms (about 24.8 days), resulting in the timeout being executed immediately.

What can I use instead of setTimeout?

Instead of setTimeout, use run.

What is the default timeout in setTimeout?

Required Syntax First of all, the setTimeout JavaScript method should contain the function that you intend to apply. The second parameter sets a time when the specified function is to be called. However, it is optional, and the default value is 0.

Does setTimeout block code?

As mentioned before once, setTimeout() is non-blocking. It will execute only after all the statements outside it it have been executed as opposed to blocking code that blocks the further execution until that operation is finished.


1 Answers

There is a copy of the current (Jun 28, 2016) implementation of window.setTimeout() in Ubuntu.

As we can see, the timer gets inserted by this line of code:

  nsAutoPtr<TimeoutInfo>* insertedInfo =
    mTimeouts.InsertElementSorted(newInfo.forget(), GetAutoPtrComparator(mTimeouts));

Then a few lines below you have an if() statement:

if (insertedInfo == mTimeouts.Elements() && !mRunningExpiredTimeouts) {
...

The insertedInfo == mTimeouts.Elements() checks whether the timer that was just inserted already timed out. The following block does NOT execute the attached function, but the main loop will immediately notice that a timer timed out and thus it will skip the IDLE state (a yield of the CPU) that you are expecting.

This clearly (at least to me) explains the behavior you are experiencing. The rendering on the screen is another process (task/thread) and the CPU needs to be relinquished for that other process to get a chance to re-paint the screen. For that to happen, you need to wait long enough so your timer function does not get executed immediately and a yield happens.

As you've notice a pause of 500ms does the trick. You can probably use a smaller number, such as 50ms. Either way it is not going to guarantee that a yield happens, but chances are it will happen if the computer on which that code is running is not currently swamped (i.e. an anti-virus is not currently running full speed in the background...)

The complete SetTimeout() function from Firefox:

(location of the file in the source: dom/workers/WorkerPrivate.cpp)

int32_t
WorkerPrivate::SetTimeout(JSContext* aCx,
                          dom::Function* aHandler,
                          const nsAString& aStringHandler,
                          int32_t aTimeout,
                          const Sequence<JS::Value>& aArguments,
                          bool aIsInterval,
                          ErrorResult& aRv)
{
  AssertIsOnWorkerThread();

  const int32_t timerId = mNextTimeoutId++;

  Status currentStatus;
  {
    MutexAutoLock lock(mMutex);
    currentStatus = mStatus;
  }

  // It's a script bug if setTimeout/setInterval are called from a close handler
  // so throw an exception.
  if (currentStatus == Closing) {
    JS_ReportError(aCx, "Cannot schedule timeouts from the close handler!");
  }

  // If the worker is trying to call setTimeout/setInterval and the parent
  // thread has initiated the close process then just silently fail.
  if (currentStatus >= Closing) {
    aRv.Throw(NS_ERROR_FAILURE);
    return 0;
  }

  nsAutoPtr<TimeoutInfo> newInfo(new TimeoutInfo());
  newInfo->mIsInterval = aIsInterval;
  newInfo->mId = timerId;

  if (MOZ_UNLIKELY(timerId == INT32_MAX)) {
    NS_WARNING("Timeout ids overflowed!");
    mNextTimeoutId = 1;
  }

  // Take care of the main argument.
  if (aHandler) {
    newInfo->mTimeoutCallable = JS::ObjectValue(*aHandler->Callable());
  }
  else if (!aStringHandler.IsEmpty()) {
    newInfo->mTimeoutString = aStringHandler;
  }
  else {
    JS_ReportError(aCx, "Useless %s call (missing quotes around argument?)",
                   aIsInterval ? "setInterval" : "setTimeout");
    return 0;
  }

  // See if any of the optional arguments were passed.
  aTimeout = std::max(0, aTimeout);
  newInfo->mInterval = TimeDuration::FromMilliseconds(aTimeout);

  uint32_t argc = aArguments.Length();
  if (argc && !newInfo->mTimeoutCallable.isUndefined()) {
    nsTArray<JS::Heap<JS::Value>> extraArgVals(argc);
    for (uint32_t index = 0; index < argc; index++) {
      extraArgVals.AppendElement(aArguments[index]);
    }
    newInfo->mExtraArgVals.SwapElements(extraArgVals);
  }

  newInfo->mTargetTime = TimeStamp::Now() + newInfo->mInterval;

  if (!newInfo->mTimeoutString.IsEmpty()) {
    if (!nsJSUtils::GetCallingLocation(aCx, newInfo->mFilename, &newInfo->mLineNumber)) {
      NS_WARNING("Failed to get calling location!");
    }
  }

  nsAutoPtr<TimeoutInfo>* insertedInfo =
    mTimeouts.InsertElementSorted(newInfo.forget(), GetAutoPtrComparator(mTimeouts));

  LOG(TimeoutsLog(), ("Worker %p has new timeout: delay=%d interval=%s\n",
                      this, aTimeout, aIsInterval ? "yes" : "no"));

  // If the timeout we just made is set to fire next then we need to update the
  // timer, unless we're currently running timeouts.
  if (insertedInfo == mTimeouts.Elements() && !mRunningExpiredTimeouts) {
    nsresult rv;

    if (!mTimer) {
      mTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
      if (NS_FAILED(rv)) {
        aRv.Throw(rv);
        return 0;
      }

      mTimerRunnable = new TimerRunnable(this);
    }

    if (!mTimerRunning) {
      if (!ModifyBusyCountFromWorker(true)) {
        aRv.Throw(NS_ERROR_FAILURE);
        return 0;
      }
      mTimerRunning = true;
    }

    if (!RescheduleTimeoutTimer(aCx)) {
      aRv.Throw(NS_ERROR_FAILURE);
      return 0;
    }
  }

  return timerId;
}

IMPORTANT NOTE: The JavaScript instruction yield, has nothing to do with what I am talking about. I am talking about the sched_yield() functionality which happens when a binary process calls certain functions, such as sched_yield() itself, poll(), select(), etc.

like image 121
Alexis Wilke Avatar answered Sep 29 '22 21:09

Alexis Wilke