2

I'm working on a web app that displays (and highlights) music notation as a song plays. Unfortunately, setInterval falls short of expectations as it's highly unreliable and skippy, even on relatively low redraw resolutions.

See this demo - all I'm doing is 'activating' a bar every x msec, and some of the blocks skip. It might not be bad on fast PCs, but once actual work is done in the loop, it will definitely fall apart.

I'm looking for a solution that will let me run a ui redraw reliably and quickly, without the unacceptable delays. If it helps, the HTML5 audio element is the master node, and all the data will be pulled from it during playback (i.e. audio.currentTime).

Any suggestions are appreciated - perhaps I'm looking for some sort of javascript threading (that doesn't seem to exist). No matter if it's not widely supported, I'm just looking to get the job done on a proof of concept here.

Jeriko
  • 6,547
  • 4
  • 28
  • 40
  • 2
    Use `setTimeout()`. A good writeup: http://stackoverflow.com/a/731625/451969 – Jared Farrish Dec 30 '11 at 20:25
  • 4
    There is no such thing as reliable timers in javascript because javascript is single threaded. If javascript is doing something at the time a timer fires, that timer event just goes in the queue and doesn't get processed until the current javascript thread of execution finishes and other events in the queue ahead of it are processed. You may need to lower your expectations and make a design that deals with some timer uncertainty. – jfriend00 Dec 30 '11 at 20:43
  • 1
    You can use a Web Worker to achieve multiple threads, see my edited answer – Paul Dec 30 '11 at 21:18
  • I agree with jfriend00. At ui redraw you have to visit each yet unvisited notation, and you better make the visit fast to make it look smooth. – Wolfgang Kuehn Dec 30 '11 at 21:28
  • As @jfriend00 says, it is not really possibly in most current browsers to precisely time a script execution in Javascript. The main difference between `setTimeout()` and `setInterval()` I know of is that `setInterval()` will try "catch up" if the browser is busy; `setTimeout()` shouldn't do that, as it's controlled by the last `setTimeout()`. I setup a [simple test](http://jsfiddle.net/jnnbS/) which tries to show the different run times that can happen; the interesting thing for me is that `setTimeout()` sometimes *under runs* the timer, which I didn't expect. I suppose my test is not correct. – Jared Farrish Dec 30 '11 at 21:46
  • [Does javascript provide a high resolution timer?](http://stackoverflow.com/questions/6875625/does-javascript-provide-a-high-resolution-timer) – katspaugh Dec 30 '11 at 22:24

4 Answers4

2

Your be_busy function doesn't actually ever enter the while loop to be busy for 100 ms. You need to flip the < to a > or >=. So those skips are just from delays in your browser getting a bit behind on the timing. But in reality those delays are insignificant if you have a proper busy function. If you flip the sign you'll get this: http://jsfiddle.net/Paulpro/PvQFh/40/embedded/result/

It is expected that it will skip over blocks like that because Javascript is single threaded. So your first call to refresh_ui will occur after 50 ms. Your next after 100 ms. And you should see the first two blocks set active.

Then at 120 ms, be_busy is called and it holds the thread for 100ms. so refresh_ui cannot be called (whether you use setTimeout or setInterval doesn't change this fact) until 220ms. refresh_ui will be called at 220 and you'll see the third block set active. Then at 240 ms be_busy will be called again. So the next refresh_ui cannot occur until 340 ms. When it does occur the 5th block will be set to active in (340/80 = 4.x).

The fourth block being skipped is expected behaviour in the best browsers. A browser which doesn't skip that block is probably lagging behind even more.

So you should have the first three blocks coloured then a a white box then 2 red ones then a white etc. and every 10 groups of two red blocks there will be a single white block.

The only real way around this is to split be_busy up into a bunch of smaller functions that split the job into pieces, or use Web Workers to do your background (busy) processing, but Web Workers will only work in very recent browsers.

Paul
  • 139,544
  • 27
  • 275
  • 264
  • Thanks for pointing out the `be_busy` bug - fixed it now (or broke it, rather :P). Almost everyone has suggested I use `setTimeout` instead, but that's not guaranteed to be regular either. Web workers might do the trick - as I said, it's just a proof of concept. Besides using a worker for the heavy lifting, do you reckon I'd get any performance gains in having the event loop in a worker too? – Jeriko Dec 31 '11 at 09:03
1

Combine set interval with your own recorded time.

Here's an example of how you might run something every second:

var startTime = new Date();
var bars = [false, false, false, false, ...] // all your bars.
var i = 0;
var intervalMs = 1000;
function doStuff() {
    var currentTime = new Date();
    if (currentTime - startTime < i * intervalMs && bar[i] != true) {
          bars[i] = true;
          i++;
    }
}

setInterval(doStuff, 10);

Make sure the interval in setInterval is much more granular than the interval you'd like to run the function.

Aside: This is a common synchronization problem in game development. Many game dev APIs render function will take an elapsed time as argument to help synchronize changes in the scene. If you have lots of things to render and synchronize, this is a good approach to take.

e.g.

MyScene.prototype.render(timeMs) {
   // calculate movements and render the scene based on elapsed time.
   MyOtherObjects.render(timeMs);
   MyObjects.render(timeMs);
   // etc.
}

var scene = new MyScene();
var lastRenderTime = new Date();
function tick() {
    var time = new Date();
    scene.render(time - lastRenderTime);
    lastRenderTime = time;
}

setInterval(tick, 1000/60); // render at 60fps

This way, if setInterval is out of sync due to cpu slow downs and other factors, the render function can re-sync the scene based on elapsed time.

Charles Ma
  • 47,141
  • 22
  • 87
  • 101
0

Instead of setInterval(), use setTimeout(). It is much more precise and reliable.

dgund
  • 3,459
  • 4
  • 39
  • 64
0

setInterval() wont give you guarantee that function will be called in time, it just stops the execution until time completes. better if you calculate before time and after time.

Ramesh Kotha
  • 8,266
  • 17
  • 66
  • 90