Scalability Problems Animate A Large Number Of Objects


Posted by integragreg apropos on Sat Feb 10th, 2007 at 22:06:04 BST

Animating a relatively large number of elements in a single diagram isn't very scalable.

At my company, we use SVG to provide rich graphical interfaces that display data in real time. For one particular application domain, we monitor the status of wind turbines and animate the turbine blades so that they appear to rotate when the turbine is generating power. Our performance specifications require us to be able to display up to 200 animated elements simultaneously in a single SVG document.

Initially, we targeted the ASV3 plug-in with IE using SMIL animation with the animateTransform element. We found that as the number of turbines on a diagram increased, so did the frequency of IE crashes and unexpected graphical behaviors. As an alternative, we began using JavaScript to simulate the animation. We thought this might provide better scalability for IE as well as allowing us to achieve animation in Firefox, since Firefox still has no support for SMIL animation.

Unfortunately, our trek down the JavaScript road hasn't yielded much better results. First, we tried simply writing our own JS function that would modify the <transform> tag attributes on a specified time interval using setTimeout(). This worked well for a small number of animated elements, but as the number increased, the animation became very choppy didn't look much better than an animated GIF. Also, the rotation in Firefox is very slow, and doesn't work at all in Opera.

Later, we came across  Doug Schepers' <a href='http://www.vectoreal.com/smilscript/'>SMILScript</a>  library. SMILScript allows Firefox to support SMIL animation by using JavaScript to provide declarative animation. Since SMILScript also uses a JavaScript setTimeout() function, the results were basically the same as when we used our own JavaScript functions.

The SVG markup included below renders an array of graphical elements that is generated and animated by a couple of JavaScript functions. Increasing the upper bounds on the For loops in the InitMe() function will illustrate the performance degradation mentioned earlier.

At this point we are still searching for a viable solution that meets our performance specification. Perhaps SVG is not a good choice for animating a large number of elements simultaneously. If anyone would like to look at the file and suggest an alternative, we'd love to hear from you.

---
<?xml version="1.0" encoding="utf-8"?>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="1200"
height="1200"
id="TurbineMapScratchPad"
version="1.0"
onload="InitMe(evt);"
>
<script type="text/ecmascript" >
    <![CDATA[

    var svgDocument = null;
    var svgRoot = null;
    var bladeRotation=0;
    var svgns = "http://www.w3.org/2000/svg";
    var xlinkns = "http://www.w3.org/1999/xlink";

    function InitMe(evt)
    {
        if ( svgDocument === null )
         {svgDocument = evt.target.ownerDocument;}

        svgRoot = svgDocument.documentElement;
        for(var y=10; y<600; y+=70)
        {
            for(var x=20; x<720; x+=40)
            {
                var gNode         = svgDocument.createElementNS(svgns,"g");
                var bladeUse    = svgDocument.createElementNS(svgns,"use");
                var baseUse     = svgDocument.createElementNS(svgns,"use");

                gNode.setAttributeNS(null,'transform','translate(' + x + ',' + y + ') scale(4.0)');
                bladeUse.setAttributeNS(xlinkns,'href','#turbineBlades');
                baseUse.setAttributeNS(xlinkns,'href','#turbineBase');

                gNode.appendChild(bladeUse);
                gNode.appendChild(baseUse);

                svgRoot.appendChild(gNode);               

            }
        }
        setInterval('animateTurbineBlades()',10);

    }

    function animateTurbineBlades()
    {
        var hubX=0;
        var hubY=0;
        var hubNode = svgDocument.getElementById('bladeHub');

        if(hubNode)
        {
            hubX=parseFloat(hubNode.getAttributeNS(null,"cx"));
            hubY=parseFloat(hubNode.getAttributeNS(null,"cy"));
        }       
        var node = svgDocument.getElementById('turbineBlades');
        if(node)
        {
            bladeRotation = bladeRotation + 2;
            if(bladeRotation > 360)
            {
                bladeRotation=0;
            }
            node.setAttributeNS(null,'transform','rotate(' + bladeRotation + ',' + hubX + ',' + hubY + ')');
        }

    }

    //]]>
</script>
<defs>

        <g id="turbineBlades">
            <path
            d="
            M 6.7203394,6.9568743
            C 5.5101085,10.802047 5.5101085,10.802047 5.5101085,10.802047
            L 5.1509285,12.769531
            L 5.1509285,12.769531
            L 7.2122104,7.0466693"
            style="fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:.100000048px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
            />
            <path
            d="
            M 7.5184653,6.8204787
            C 11.453598,5.945983 11.453598,5.945983 11.453598,5.945983
            L 13.337079,5.2733
            L 13.337079,5.2733
            L 7.3502946,6.3496084"
            style="fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:.100000048px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
            />
            <path
            d="
            M 7.0542563,6.2458097
            C 4.3293545,3.2751324 4.3293545,3.2751324 4.3293545,3.2751324
            L 2.8050536,1.9803314
            L 2.8050536,1.9803314
            L 6.7305561,6.626885"
            style="fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:.100000048px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
            />
            <circle id="bladeHub" cx="7.1" cy="6.6" r="0.5" style="fill:black" />

        </g>
        <g id="turbineBase">
        <path
            d="
            M 6.8684577,7.1257859
            C 6.8684577,11.288829 6.8684577,11.288829 6.8684577,11.288829
            L 7.3010127,11.288829
            L 7.3010127,7.1257859
            L 6.8684577,7.1257859
            L 6.8684577,7.1257859"
            style="fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:0.40460387px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
        id="baseUpperNew" />
        <path
            d="
            M 6.5893374,11.295157
            C 6.5893374,18.210662 6.5893374,18.210662 6.5893374,18.210662
            L 7.5738901,18.210662
            L 7.5738901,11.295157
            L 6.5893374,11.295157
            L 6.5893374,11.295157"
            style="fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:0.78674704px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
        id="baseLowerNew" />
        </g>
</defs>

</svg>   

Display: Sort:
Suggestions (5.00 / 2) (#2)
by ed on Tue Feb 13th, 2007 at 20:00:29 BST
(User Info)
Initially, we targeted the ASV3 plug-in with IE using SMIL animation with the animateTransform element. We found that as the number of turbines on a diagram increased, so did the frequency of IE crashes and unexpected graphical behaviors. As an alternative, we began using JavaScript to simulate the animation. We thought this might provide better scalability for IE as well as allowing us to achieve animation in Firefox, since Firefox still has no support for SMIL animation.

I did some permutations on your provided source to see what gives better performance. I couldn't get any script version to run faster/smoother than a corresponding SMIL animation version.

Some suggestions:

  1. combine the path elements into one by concatenating the d-attributes, one path element for the animated part, one path element for the static part
  2. drop the style attributes
  3. if you're still going with a script version insert only one <use> element instead of three elements
  4. hubX/hubY are static, so hardcode them instead of doing parseFloat
  5. try doing cloneNode on the turbine group instead of creating <use> elements, and build an array of the elements you want to animate as they are inserted, then update the transform attribute on those elements


Another option that I explored was to use a <pattern> of the turbine. A pattern is similar to a texture. Then you get faster tiling as well as animation synchronization almost for free. The downside is that using the pattern will be great if you use it on one element, and is best if you have a quite regular structure. Your example animated much more quickly when using a pattern containing the turbine on a rect covering the entire viewport.

In the end I would opt for a solution being something like this:

  • a "turbine" group containing two subgroups, one animated "turbineBlades" and one static "turbineBase"
  • the "turbineBlades" group gets an <animateTransform> element that does the rotation
  • the init-function in script then inserts the desired number of <use> elements with xlink:href="#turbine"


Updated Code: Better Performance (5.00 / 1) (#3)
by integragreg on Sat Feb 24th, 2007 at 17:05:06 BST
(User Info)
Having implemented the two very helpful suggestions from the responses to my post, here is an updated version of the code.  Tor is correct, of course, in his suggestion for Firefox.  Memory consumption is greatly reduced in the new version, although the animation is still a bit choppy. Perhaps frame-based animation would be a better approach for me to take?
----------
<?xml version="1.0" encoding="utf-8"?>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="1200"
height="1200"
id="TurbineMapScratchPad"
version="1.0"
onload="InitMe(evt);"
>
<script type="text/ecmascript" >
    <![CDATA[

    var svgDocument = null;
    var svgRoot = null;
    var bladeRotation=0;
    var svgns = "http://www.w3.org/2000/svg";
    var xlinkns = "http://www.w3.org/1999/xlink";
    var svgUserAgent = null;

    function InitMe(evt)
    {
        svgUserAgent = navigator.appName;

        if ( svgDocument === null )
         {svgDocument = evt.target.ownerDocument;}   

        svgRoot = svgDocument.documentElement;
        for(var y=10; y<600; y+=70)
        {
            for(var x=20; x<720; x+=40)
            {
                var index = x*y;
                var gTurbine         = svgDocument.createElementNS(svgns,"g");
                var gBlades = svgDocument.createElementNS(svgns,"g")
                var gBase = svgDocument.createElementNS(svgns,"g");
                var bladesUse    = svgDocument.createElementNS(svgns,"use");
                var baseUse = svgDocument.createElementNS(svgns,"use");
                var transNode = svgDocument.getElementById('animTrans');

                gTurbine.setAttributeNS(null,'id','turbine');
                gTurbine.setAttributeNS(null,'transform','translate(' + x + ',' + y + ') scale(4.0)');
                gBlades.setAttributeNS(null,'id','blades');

                gBase.setAttributeNS(null,'id','base');

                bladesUse.setAttributeNS(xlinkns,'href','#bladePath');
                bladesUse.setAttributeNS(null,'id','bladeUse');
                baseUse.setAttributeNS(xlinkns,'href','#basePath');

                gBase.appendChild(baseUse);
                gBlades.appendChild(bladesUse);
                if(svgUserAgent != "Netscape" && null != transNode)
                {                   
                    var newTrans = transNode.cloneNode(true);
                    gBlades.appendChild(newTrans);
                }
                gTurbine.appendChild(gBase);
                gTurbine.appendChild(gBlades);                
                svgRoot.appendChild(gTurbine);
            }
        }
        if(svgUserAgent == "Netscape") //Mozilla Firefox       
        {
            setInterval('animateBladesFF()',10);           
        }

    }

    function animateBladesFF()
    {
        root = document.getElementById("TurbineMapScratchPad");
        id = root.suspendRedraw(1000);
        var hubX=7.1;
        var hubY=6.6;

        var animList = svgDocument.getElementsByTagName("use");
        for(var i=0; i<animList.length; i++)
        {
            var node = animList.item(i);
            if(node.id=='bladeUse')
            {
                bladeRotation = bladeRotation + 2;
                if(bladeRotation > 360)
                {
                    bladeRotation=0;
                }
                node.setAttributeNS(null,'transform','rotate(' + bladeRotation + ',' + hubX + ',' + hubY + ')');
            }
        }
        root.unsuspendRedraw(id);
    }

    //]]>
</script>
<defs>
            <animateTransform id="animTrans" attributeName="transform"
                type="rotate" from="0,7.1,6.6" to="360,7.1,6.6" begin="0s" dur="3.0s"
                repeatCount="indefinite"
            />
            <g id="turbineBlades">
                <circle id="bladeHub" cx="7.1" cy="6.6" r="0.5" style="fill:black"/>
                <path id="bladePath"
                    d="
                    M 6.7203394,6.9568743
                    C 5.5101085,10.802047 5.5101085,10.802047 5.5101085,10.802047
                    L 5.1509285,12.769531
                    L 5.1509285,12.769531
                    L 7.2122104,7.0466693
                    M 7.5184653,6.8204787
                    C 11.453598,5.945983 11.453598,5.945983 11.453598,5.945983
                    L 13.337079,5.2733
                    L 13.337079,5.2733
                    L 7.3502946,6.3496084
                    M 7.0542563,6.2458097
                    C 4.3293545,3.2751324 4.3293545,3.2751324 4.3293545,3.2751324
                    L 2.8050536,1.9803314
                    L 2.8050536,1.9803314
                    L 6.7305561,6.626885"
                />

            </g>
            <g id="turbineBase">
                <path id="basePath"
                    d="
                    M 6.8684577,7.1257859
                    C 6.8684577,11.288829 6.8684577,11.288829 6.8684577,11.288829
                    L 7.3010127,11.288829
                    L 7.3010127,7.1257859
                    L 6.8684577,7.1257859
                    L 6.8684577,7.1257859
                    M 6.5893374,11.295157
                    C 6.5893374,18.210662 6.5893374,18.210662 6.5893374,18.210662
                    L 7.5738901,18.210662
                    L 7.5738901,11.295157
                    L 6.5893374,11.295157
                    L 6.5893374,11.295157"
                  />
            </g>

</defs>
</svg>    

try opera (5.00 / 1) (#5)
by antonio caccamo (antonio@coguaro.org) on Mon Mar 5th, 2007 at 12:21:45 BST
(User Info)
I found Opera more faster than firefox in svg rendering.
Here you can find opera svg support list:
http://www.opera.com/docs/specs/svg/

hope will be helpful =============== antonio caccamo

updated code (5.00 / 1) (#6)
by boxster on Mon Jun 25th, 2007 at 00:28:49 BST
(User Info) http://0am.de
Thanks for the updated code, it is much faster than the original one.

Tuning for Firefox (3.50 / 2) (#1)
by tor on Tue Feb 13th, 2007 at 16:42:11 BST
(User Info)

This example has poor performance on Firefox due to the way we currently implement <use>, which is by doing an anonymous clone of the content tree. Updates to the original cause a reclone, which if <use> is used lightly isn't a big problem, but as in your example you have a large number of cloned objects to be updated each frame it will cause problems.

It might be faster to do something like the following:

  root = document.getElementById("TurbineMapScratchPad");
  id = root.suspendRedraw(1000);
  ... loop through each <use> and assign them a transform ...
  root.unsuspendRedraw(id);

Display: Sort:
Login

Make a new account

Username:
Password: