Speeding Up Frame Rates For DHTML Animation in Win98 | WebReference

Speeding Up Frame Rates For DHTML Animation in Win98

Speeding Up Frame Rates For DHTML Animation in Win98: A Case Study.  

Copyright © 2000  Mark Szlazak

Last Updated July 9th, 2000

Introduction

DHTML is often used to move page elements and create web page animation. To achieve smooth, strobe free motion, a browser (in conjunction with it's operating system and hardware) must update the screen at both fast enough and regular enough rates to avoid strobe effects.

Television uses a rate of 30 images per second (30 Hz) and in the movies it's 24 frames per second (24 Hz). For natural motion, a regular update rate of 16 Hz is considered the minimum that's just adequate for many moving things. However, this will depend on the velocity and the sharpness of the items that are moving. For a fast moving sharp object 100 Hz isn't enough! You can see that by waving a finger back and forth in front of your monitor. You'll see a strobe effect rather than smooth motion. Say your monitor updates at 100 Hz (10 ms per frame) and the image element is incrementally moved at 5 pixels/frame, then there will be a physical jump on the retina associated with the 5 pixel displacement between each position and the temporal update will be converted to a spatial duplication of the image element that the brain will pick up. When one sees strobe motion one sees multiple copies at once. Decreasing the size of each movement step will bring the copies closer together and smooth the animation but this then decreases the maximum speed at which you can smoothly move the image around. So, increasing image update rates beyond 16 Hz will increase the maximum speed at which you can smoothly move field elements.

Sharpness of the moving image is another factor that relates to strobe motion and frame rates. Take a video movie and run it one frame at a time. You will see that there are huge jumps taking place all the time that aren't noticed in complex scenes. The background masks jumps. However, with simpler backgrounds, like a finger moving on a static background of a monitor, the same motion is seen with strobe effects.

Even though you may have a fast enough frame rate for the velocity you need on the background your using, this still may produce unacceptable animation. The stability of the rate needs to be considered. A fast enough but unstable frame update rate could cause jerkiness or unwanted decelerations or accelerations during image movement. An adequate series of updates followed by a slow update or series of updates could cause jerkiness or deceleration if the slower rate falls to low or accelerations if the series rate goes to high. In other words, an update rate also needs to stay within some critical range often enough to produce a good effect.

Morten Wang's past Web Reference article [1], demonstrated one of the limitations of standard DHTML animation techniques on Win9x systems. Basically, they don't achieve update rates greater than 18 Hz, limiting how fast you can smoothly move things on your web page.

This present article explains and demonstrates some multithread techniques in JavaScript that speed up element position updates 50% to 27 Hz. The codes are simple, work in both Internet Explorer (IE) and Netscape's Navigator (NN) browsers, and have the same structured as existing DHTML animation scripts so modification is easy. At 27 Hz with a movement increment of 1 pixel, the method can produce motion nearly as smooth as the standard, 18 Hz, 1 pixel increment, technique and many times smoother animation than the standard 18 Hz technique incrementing at 2 pixels. However, this performance is not consistent. Runs can vary in jerkiness on the same browser and difference between browsers is significant with IE out performing NN on all versions of multithreaded code looked at here.

All tests for this single system case study were performed on a Windows 98 2nd ed, 90 MHz Pentium P54CS machine with a Number 9 GXE64 Pro 2Mb graphics card. The browser versions used were IE 5.01 and NN 4.7.


Animation In DHTML

When you want to make web page elements move across the screen using DHTML, you can execute a timed loop: move'em a little, wait, move'em a little, wait...etc., and a script of 200 loops to move page elements could look like this:

    function runIt() {
      for (i=0;i<200;i++) moveIt();
    }
    
    Many readers may already know what this technique of animation will look like. If your unfamiliar with its results then you can see them for yourself and make note of the difference in the way it turns out on NN and IE.

    NN displays many of the intermediate image positions between start and finish but IE doesn't display any of them at all. IE actually does move the image but this happens "behind the scenes" and only the first and last positions are displayed. The reason for this is that whenever a script is evaluated, the IE parsing agent takes over, interprets the script first, and then only after the script terminates does the display get updated [2]. In this case the last position gets the display update when the for()-loop finishes and an exit occurs from the executing outermost function, runIt().

    Suppose that this wasn't the situation and Explorer worked like Navigator. Even so this script would still have problems. For instance, the for()-loop method has no easy control over the speed of animation and the loop runs as fast as the browser will let it. This is a problem for the computer monitor that can't update as fast as the loop runs and misses some changes in position between updates. Moreover, as long as the for()-loop is executing, the browser is basically locked out from capturing other events or executing other script. This loop virtually gains control over the browser by giving no time slices for these other things to happen until it finishes. To avoid all these problems in animation two JavaScript methods are used that thread script execution.


    JavaScript Threading

    In animations, the JavaScript methods setTimeout("script expression", millisecond delay) and setInterval("script expression", millisecond delay) are used to form timed loops that update element positions and the browser's display. These methods exit there own processing thread and essentially invoke a new thread, which after a set delay period executes independently of the rest of the script. The display then gets updated with an exit from this new thread [2]. In an animation loop, this continuously repeats with each cycle of these timers. Web page elements are moved a little, displayed and the delay setting controls the movement speed and update rate.

    In both methods, delays can be set with millisecond resolutions. However, as we'll see shortly and except for one case, the actual delays provided do not have this resolution at all. Never the less, the delay period not only delays the referred to expressions execution (here the evaluated expression will be a function call, moveIt(), to the movement script) but also provides a "time window" for event capture and for other script to run. These timeouts don't halt other tasks from executing! If the setTimeout() method or the setInterval() method were in the middle of a script, the statements just after it would execute immediately. It's only the expression the method refers to that's delayed from running.

    Each call to setTimeout() delays the single evaluation of its expression. To get  repeatedly scheduled evaluations for an animation loop, setTimeout() can be placed inside the incremental movement function, recursively calling the function as in the code below.

      var halt;
      function moveIt() {
       // Your incremental image stepping script goes here.
       if (!halt) setTimeout("moveIt()",milliseconds);
      }
      setTimeout("moveIt()",milliseconds);
      
      This coding structure places the delay before each repeated execution of the script so it executes outside the delay period provided by the timer. I'll represent timer delays with square brackets around underscores
        timer delay: [__________]
        
        and animation script delays as vertical bars around periods
          animation script delay: |...|
          
          so the time line for the running code looks like this:
            [__________]|..|[__________]|.|[__________]|...|[__________]|.| etc.
            On average, the timer delays are approximately the same but this may not be the case for the scripts delay. The script that actually moves the image could happen at different speeds on the same machine or could vary between machines depending on the processor and to some extent the browser. So the time between starts of the script will vary beyond that of the delays. 

            Another coding structure that doesn't intersperse animation script delays between the timer delays, places setTimeout() before the script. The animation script executes immediately after the timeout statement inside the delay period provided by the timer. Time between starts of the script is more constant and updates should proceed more quickly but still vary with script delay. 

              var halt;
              function moveIt() {
               if (!halt) setTimeout("moveIt()",milliseconds);
               // Your incremental image stepping script goes here.
              }
              setTimeout("moveIt()",milliseconds);
              The time line looks like this:
                [|..|______][|.|_______][|...|_____][|.|_______] etc.
                
                The third way to code an animation loop is to use the setInterval() method. A call to setInterval() sets up a repeated scheduling of the evaluation of its expression. It doesn't have to be coded to repeatedly call itself like setTimeout() since one call does this automatically. The time line looks like that of the second setTimeout() coding technique where the animation script delays are not added to the timer delays.
                  var timerID;
                  function moveIt() {
                    // Your incremental image stepping script goes here.
                  }
                  timerID = setInterval("moveIt()",milliseconds);
                  

                  Time Resolution of SetTimeout(), SetInterval and Date().

                  The second parameter in both setTimeout() and setInterval() sets the delay in milliseconds. However as alluded to above, there are problems with this setting. Regardless of whether you try setting the delay below 55 ms, these methods can't give delay's less than 55 ms in Win9x. What maybe more surprising is that settings above 55 do not correlate with desired delays except for setInterval() when run on NN. For both IE and NN on Win9x, setTimeout() only gives delays that are integer multiples of 55 ms. Settings 1-55 give 55 ms, 56-110 give 110 ms, 111-165 give 165 ms, and so on, limiting the choice of frame rates to 18, 9, 6, 4.5, 3.6,...etc, Hz. This stepping of rates is apparently due to Win9x timers that update to the 55 ms time slice precision of the computers system clock [3]. On IE setInterval() behaves like setTimeout() does but NN does not. NN gives the delays you set at as long as the settings are above 55.

                  A correlation study of delay settings and actual delays of the setTimeout() and setInterval() methods can be done using the Date() object. Date() creation is relatively fast at about 0.1-0.5 ms [1]. However, measuring process delays with two Date() creations, one just before a process starts and one just after it ends, is again limited to the 55 ms precision of the computers clock on Win9x. To see this you can use for()-loops of increasing size as the processes to be timed and correlated loop size with the delays returned. The code could look like this:

                        t1 = new Date();
                        for(j=0;j<loops;j++);
                        t2 = new Date();
                        delay = t2-t1;
                    
                    It turns out that the shortest returned delays never go between 0 and 50. Also, for larger and larger loop sizes, the delays come back as multiples of 55 (+/- 5). The values that I've seen are: 0, 50, 60, 110, 160, 170, 210, 220, 270, 280, 320, 340...
                     to see this for yourself.
                    This means that individual processes delays can't be measured with millisecond precision using Date() but they can be estimated for a group of identical or similar processes.

                    Say an actual individual processes delay is 1 ms. If we place t1 = new Date() just before a 100 of these and t2 = new Date() just after then a delay (= t1-t2) is returned. The estimate for each individual process is simply delay/100 ms-per-process. Now, if these processes start at the beginning of a time slice, overlapping two of them, then the delay returned would be 55 (actually 50 or 60) and the individual estimated delay would be 0.55 ms. On the other hand, if they started more into a time slice, overlapping three of them, then the delay returned would be 110 giving an individual delay of 1.1 ms. The error in the individual estimate would be either 0.45 ms or 0.1 ms. If this error is too large then simply increase the number of processes measured.

                    Since, we're dealing with process that are theoretically no faster than 1 ms, and looking for general patterns, this simple approach is quite adequate to correlate the delay setting with actual delays for both setTimeout() and setInterval().

                    For setTimeout() the test script looks like this:

                      function testIt() {
                        if (ctr<n) {
                          ctr++; 
                          timerID = setTimeout("testIt()",setDelay);
                        }
                         else {
                           arrival = new Date();
                           upDate();
                        }
                      }
                      function startTest() {
                        ctr=0;
                        departure = new Date();
                        timerID = setTimeout("testIt()",setDelay);
                      }
                      function upDate() {
                        delay = arrival - departure;
                        // Stats and book keeping.
                        // restart test if need be by calling startTest() or stop.
                      }
                      
                      For setInterval() it looks like this:
                        function testIt() {
                          if (ctr<n) {ctr++;}
                           else {
                             arrival = new Date();
                             clearInterval(timerID);
                             upDate();
                           }
                        }
                        function startTest() {
                          ctr=0;
                          departure = new Date();
                          timerID = setInterval("testIt()",setDelay);
                        }
                        function upDate() {
                          delay = arrival - departure;
                          // Stats and book keeping.
                          // restart test if need be by calling startTest() or stop.
                        }
                        

                        To run the tests just click below.

                        Morten Wang has examined the correlation between set delay values and actual delays for the setInterval() method on Win95/98, WinNT, Win2K and Linux [4]. He found the same pattern. IE steps delay settings to a multiple of the lowest possible delay, the difference being the lowest delay possible between the operating systems. Again, with setInterval() in NN any setting larger than the lowest possible delay is actually what you get.

                        Two methods cancel the scheduled execution of the thread before the delay is up. The clearTimeout(timerID) method cancels the scheduled timerID = setTimeout() timer that's waiting to evaluate its expression. Just clearTimeout() is used when a name isn't assigned to setTimeout(). The clearInterval(timerID) method cancels the automatic rescheduling of timerID = setInterval() timer's expression evaluation.


                        Speeding Up The Animation Loop With Multithreading

                        The technique for speeding up the animation loop basically spawns two threads one just after the other. Each thread schedules a call to moveIt() after the minimum 55 ms delay. This increases the number of visible updates within a given time period and doesn't lockout the browser from other events and script. Within the same browser, their performance varies from less stable animation over the standard single thread method in certain page environments to better animation in others. Between the two browsers, IE performs much better than NN with these multithread speedups.

                        Our initial animation will provide a simple context to start looking at various bi-threaded scripts.

                        The first bi-threaded script has a coding structure like that of the second single thread setTimeout() animation loop discussed previously but initializes with two successive timeout calls.

                          function moveIt() {
                           if (!halt) {
                             setTimeout("moveIt()",1);
                             // Your incremental image stepping script goes here.
                           }
                          }
                          setTimeout("moveIt()",165);
                          setTimeout("moveIt()",165);
                          
                          Walking through the script beginning at time 0 for the initially scheduled threads gives:
                          • t-0:
                              Schedule first & second moveIt() threads in about 165 ms & 165+d ms.

                          • t~165: THREAD ONE.
                              Schedule moveIt() thread in 55 ms at "t~220".
                              Increment images.
                              Exit thread one & update display.

                          • t~165+d: THREAD TWO.
                              Schedule another moveIt() thread in 55 ms at "t~220+d".
                              Increment images.
                              Exit thread two & update display.

                          • etc...

                          You can compare the standard 18 Hz, 1 and 2 pixel stepping animations with the 27 Hz, 1 pixel stepping animation using the above technique by clicking below. The 27 Hz rate was measured by noting the time between two distinct events in the animation using the second hand of a wristwatch.

                          Here are some variations on the same theme that essentially have the same performance in our example.

                          Index each of the two thread calls, have one thread do no rescheduling and have the other thread handle the rescheduling of both of them.

                            var timerID1, timerID2;
                            function moveIt(thread) {
                             if (!halt) {
                              if (thread==2) { 
                                timerID1 = setTimeout("moveIt(1)",1);
                                timerID2 = setTimeout("moveIt(2)",1);
                              }
                              // Your incremental image stepping script goes here.
                             }
                            }
                            timerID1 = setTimeout("moveIt(1)",165);
                            timerID2 = setTimeout("moveIt(2)",165);
                            
                            A couple more possibilities place the setTimeout()'s at the end of the movement script instead of at the beginning of the function as in the above two codes.

                            With setInterval() one script could look as follows. Note that this script requires browser detection since it will run very erratically on Netscape with delay settings of 1 in both timers, almost locking out the browser. Fortunately delay settings like 50 ms and 75 ms in this method are recognized as actually being 25 ms apart in Navigator and will allow the code to function.

                              var timerID1, timerID2, delay1, delay2;
                              function moveIt() {
                               if (!halt) {
                                // Your incremental image stepping script goes here.
                               }
                              }
                              if (document.all) { delay1=1; delay2=1; }
                              if (document.layers) { delay1=50; delay2=75; }
                              timerID1 = setInterval("moveIt()",delay1);
                              timerID2 = setInterval("moveIt()",delay2);
                              
                               to see the animation speedup with this method.

                              In analogy to the second multithread setTimeout() method, one setInterval() could continuously clear itself after each iteration while the other takes care of rescheduling it again.

                                var timerID1, timerID2, delay1, delay2;
                                function moveIt(thread) {
                                 if (!halt) {
                                  if (thread != 2) clearInterval(timerID1);
                                   else timerID1 = setInterval("moveIt(1)",delay1);
                                  // Your incremental image stepping script goes here.
                                 }
                                }
                                if (document.all) { delay1=1; delay2=1; }
                                if (document.layers) { delay1=50; delay2=75; }
                                timerID1 = setInterval("moveIt(1)",delay1);
                                timerID2 = setInterval("moveIt(2)",delay2);
                                

                                A hybrid technique can use one setInterval() that's always running and a setTimeout() that the setInterval() will reschedule.

                                  var timerID1, timerID2, delay1, delay2;
                                  function moveIt(thread) {
                                   if (!halt) {
                                    if (thread == 2) timerID1 = setTimeout("moveIt(1)",delay1);
                                    // Your incremental image stepping script goes here.
                                   }
                                  }
                                  if (document.all) { delay1=1; delay2=1; }
                                  if (document.layers) { delay1=50; delay2=75; }
                                  timerID2 = setInterval("moveIt(2)",delay2);
                                  
                                  These speedups ran many times about as smooth as standard methods on IE but for NN they're more jerky. Even for IE as the number of moved images goes up the jerkiness increases. I doubled and quadrupled the number of images to four and eight and the bi-threaded techniques just didn't handle the movements as smoothly, possibly because the browser can't update that many images at 27 Hz but can at 18 Hz. With NN, animations degrade more quickly and do worst than IE in all these cases, single or double threaded. This is especially so with the bi-thread techniques which also nearly lockout the browser when four or more images are being moved.
                                  Using setTimeout():  
                                  Using setInterval():

                                  This next context looks at image rescaling as a different type of increase in rendering complexity. The three images used in the previous animations could have come from large files, say 888 by 600 pixels for the Windows 98 background and 888 by 888 pixels for each of the moving NN and IE logos, which the browsers would then have to rescale for display on the page. This places a very different workload on one browser over the other. The animation with two moving images will be used to illustrate what happens. 

                                  In IE, using standard methods, the rescaling causes the image within it's moving box to wobble and box motion to degrade, but NN has no such problems and it's performance remains consistent with it's previous non-rescaling examples. Runs in either the bi-threaded-speedup or using a two-pixel-increment have less apparent wobble than in the single threaded one-pixel-increment movements and could possibly be used as alternatives to help stabilize a wobbly IE animation.

                                  This example may seem bit contrived and unrealistic but it's not. First of all, web applications are scaled to viewer requirements. Office 2000's Power Point was designed to be web-friendly, its files can be save as DHTML and this involves scaling of image presentations. Secondly, the effects on animation in these rescaling examples do not only occur in these cases, but are representative of many other types of real world situations which degrade IE's abilities when certain page complexities increase. In these same situations NN's performance remains fairly constant. According to DHTML Lab's editor, Peter Belesis, in applications that he works on at his company, "the Netscape version has about 4000 layers on one page, with no degradation in performance. IE degrades with 50, so I have had to break up the page and use workarounds. NS DHTML was created for animations. IE DHTML is more suited for intranet data management and component development" [12].

                                  Forgetting any multitasking for the moment, and looking just at the different DOM's of these two browsers will give reasons for their varying performance in different contexts. When IE re-renders it does this to the entire page but NN doesn't have to. To have the ability to modify any element in the page, IE's very fast rendering engine needs to re-calculate the entire page every time a change is made. In the image resize example, IE re-renders the entire page with every image movement, rescales all images including those that have already been scaled. This extra workload shows up as wobble. NN has a simpler DOM with a "layered" architecture, modifications cannot be made to any element, but elements can be positioned outside the normal flow of the page. NN doesn't need to re-calculate the entire page to move an independent layer and it happens to be the case that image resize occurs only once outside of layer movement. During layer movement, NN isn't burdened with extra tasks like IE and movement quality remains more consistent.

                                  Now in the previous situation, no rescaling was required with any of the images and any extra work occurred only as more and more images were added to the movement. The tasking conditions were essentially the same and the increases in complexity were essentially the same on both browsers. They only had to move more and more images around and IE just happened to perform this a little better.  

                                  Back to multithreading. To make things go faster and barring any limitations on the systems display update rate for the moment, one could extent these bi-threaded speedups by simply creating more threads. I've tried using three threads with my two moving images and this does speed up the animations but all the runs are quite jerky on IE and very erratic on NN.


                                  Stability Problems and Limitations With Standard and Multithread Scripting Methods

                                  Whether one uses standard single thread techniques or the multithread techniques illustrated here, they all have problems with stability. They can start rough, smooth themselves out some but unwanted accelerations, decelerations and jerks remain. Some runs being worst than others. Setting the window object offScreenBuffering property always to true doesn't help much. Unlike IE which is multithreaded, NN is single threaded so its poor performance isn't surprising with the multithreaded scripts. However, even though IE does accommodate these methods much better, its performance isn't as uniform as it could be and with "tri-threaded" codes is just unacceptable.

                                  The multithreading model for IE is basically "apartment" threading but the scripting engines implement a limited threading independent from the rest of the environment [5]. The setTimeout() and setInterval() methods providing the course rudimentary capability for this. This limited threading ability could easily account for IE's less than optimal behavior with two threads and poor behavior with more threads than that.

                                  Overcoming these limitations would require that future versions of the Win9x OS use a faster hardware clock instead of the 55 ms resolution system clock to base off. WinNT and Win2K base off the memory controller in the chipset and have a 10 ms timer resolution. With Win2k+ eventually replacing Win9x, WinMe, etc., scripts would be able to take advantage of this faster timer [3],[11], giving up to 100 Hz updates which exceeds the ability of most PC monitors. Also, a more sophisticated version of the JavaScript engine could incorporate a better threading environment to deal with current speed and stability problems. Until things like this happen, we are stuck with a minor ability in Win9x to move page elements smoothly in DHTML. A solution could be to use techniques that go beyond the limitations of scripting alone. Packages like Macromedia's Flash [6] or Shockwave are optimized for creating animations, they're cross-platform, and have good development environments and common plugins for most browsers. Still, Flash and Shockwave cannot escape their animation box to interact with other page elements. They are not integral parts of a web page like DHTML and give only a partial solution. 


                                  Acknowledgements

                                  I would like to thank Morten Wang, Tim Williams, Kurt Cagle and Stanley Klein for their valuable insights and suggestions. A special thanks goes to DHTML Lab's editor, Peter Belesis, for the very helpful comments on browser performance in different settings and points on browser architecture. Everyone's "pearls of wisdom" have contributed to make this article much better than it would have been without them.

                                  References

                                  1.   Wang, M: "Smooth animation using DHTML: Can it be done?", In WebReference.com's "Dynamic HTML Lab", Jan. 3, 2000, Internet.com Corp. http://webreference.com/dhtml/column28/

                                  2.   Cagle, K: "DHTML Window Redraw", In Inquiry.com's "Ask The DHTML Pro.", Dec. 23, 1998, DevX.com, Inc. http://www.inquiry.com/techtips/dhtml_pro/answer.asp?pro=dhtml_pro&docid=2508

                                  3.   Petzold, C: "Programming Windows. The definitive guide to the Win32 API", ed 5, Redmond, Washington, 1999, Microsoft Press, pp 327-330.

                                  4.   Morten Wang: Personnel communication. June 6th, 2000.

                                  5.   Kurt Cagle: This suggestion comes from Kurt Cagle but without any proof. June 27th, 2000.

                                  6.   Levy, K: "Working With Flash", In Webreference.com's "Multimedia", June 23, 2000, Internet.com Corp. http://webreference.com/multimedia/flash/

                                  7.   Goodman, D: "JavaScript Bible", ed 3, Foster City, CA, 1998, IDG Books Worldwide Inc.

                                  8.   Flanagan, D: "JavaScript: The Definitive Guide", ed 3, Sebastopol, CA, 1998, O'Reilly & Assoc.

                                  9.   Goodman, D: "Dynamic HTML: The Definitive Guide", Sebastopol, CA, 1998, O'Reilly & Assoc.

                                  10.   Rule, J: "Dynamic HTML: The HTML Developer's Guide", Reading, MA, 1999, Addison Wesley Longman, Inc.

                                  11.   Russinovich, M: "Inside Windows NT High Resolution Timers", 1997, Mark Russinovich. http://www.sysinternals.com/timer.htm

                                  12.   Peter Belesis: Personnel communication. July 8th, 2000.