Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
172 views
in Technique[技术] by (71.8m points)

javascript - 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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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
",
                      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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...