Log in to like this post! NetAdvantage For jQuery Chart – An HTML5/jQuery Chart Designed for Performance! Graham Murray / Wednesday, January 11, 2012 Our new (CTP) NetAdvantage jQuery Chart, like its sibling, the XamDataChart is designed from the ground up for performance. Our customers can't get enough of the extreme performance available in the XamDataChart for Silverlight and WPF so we are doing our best to mirror its engine in our implementation of a data chart for jQuery using the HTML5 Canvas control for rendering. Our jQuery Chart (CTP) is engineered to help support high frequency update scenarios, extreme amounts of data, and animation. I've devoted previous blog posts to each of these areas, see: High frequency updates Using the NetAdvantage jQuery Chart and SignalR to Display a Live Data Stream Extreme amounts of data Using the NetAdvantage jQuery Chart to Explore Over One Million Items of USGS Data Animation Using NetAdvantage jQuery Chart, Infragistics Motion FrameworkTM and SignalR to Display Live Twitter Rates Adding History and Trend Lines to Our Live Twitter Chart But in this post, how about let's talk some performance details and numbers, and then see how far we can push it before your browser's JavaScript engine gives up and quits. The sample we will build today (with Live Demo!) will focus on measuring the performance of the High Frequency Updates scenario. But first let's talk about the existing live demos we have of the chart's performance and the numbers they provide. Binding High Volume Data In the samples for NetAdvantage for jQuery v11.2 we shipped a sample for binding extreme amounts of data to the chart. You can visit this sample here: https://www.igniteui.com/data-chart/binding-high-volume-data The sample seperates the generation of the sample data from its assignment to the chart via the use of 2 separate buttons. This helps you distinguish the amount of time it takes to generate the data from how long it takes the chart to present the data. First, use the slider to select how many values you'd like to assign to the chart, then click "Generate New Data" and then "Assign Data". Here are the numbers that I get for various browsers on my machine: Browser 10,000 points 100,000 points 500,000 points 1,000,000 points Google Chrome (15.0.874.121 m) 22ms 34ms 174ms 426ms Internet Explorer (9.0.8112.16421) 31ms 60ms 348ms 669ms Safari (5.1.1 (7534.51.22)) 18ms 33ms 122ms 241ms Firefox 8.0 27ms 68ms 384ms 812ms Opera (11.52) 26ms 51ms 134ms 264ms Notice also, that after you bind even 1,000,000 points to the chart you can zoom in and out (mouse wheel, or page up/page down), and everything stays smooth. 1 Million points? Our chart eats one million points for breakfast. You will likely run out of memory alloted to the page before you cause the chart a significant headache. Safari seems to edge out the other's here. Achieving this level of consistent performance across all these browsers was a rather challenging feat, and is something we will continue to work on as we bring the control to RTM. Some of the browsers, while having fast and hardware accelerated canvases, are less good at array processing or optimizing JIT compiled JavaScript, so it was a remarkable challenge to process this much data and present it efficiently cross-browser. Our jQuery chart, like the XAML XamDataChart, figures out the best ways to visually virtualize the data, without losing fidelity, to render it very efficiently on the target platform. Increasing the amount of records in the data set bound to the chart may up the data processing complexity, but the chart will aggressively manage the visual complexity of what is rendered. So, even with extreme amounts of data, the chart performs well. Binding High Frequency Data In the samples for NetAdvantage for jQuery v11.2 we shipped a sample for high frequency real-time data to the chart. You can visit this sample here: https://www.igniteui.com/data-chart/binding-real-time-data In this sample, let's drag the slider for Refresh Time to 10. This will attempt to add a point to the end of the chart and remove a point from the beginning of the chart every 10ms creating a "sliding window" effect. And let's start out with a small number of values in Data Points and work our way up to higher values. Data Points describes the constant amount of values there are in the chart at any given time. But, in every case, we are removing 1 point and adding 1 point every 10ms. Assume the version numbers of the browsers are the same as above. Frames per second is being measured in the chart by listening to the refreshCompleted event on the chart, which fires every time the chart is completely finished rendering into the canvas. These FPS values are a bit non-scientific as the sample doesn't provide a running average of the fps, so I'm trying to pick a median value by sight: Browser 100 points 1,000 points 10,000 points 50,000 points Chrome 78fps 40fps 40fps 38fps IE9 72fps 20fps 18fps 19fps Safari 92fps 30fps 31fps 31fps Firefox 86fps 32fps 32fps 26fps Opera 58fps 26fps 26fps 24fps Here we see that, although there is a performance degradation when moving between a small number of points in the window to 1,000+ points, the browsers have pretty steady performance between 1,000 points through 50,000 points in the window. The visual virtualization is working to keep the line easy to render while retaining high fidelity to the represented data. And if you zoom in far enough you can visually resolve each individual data point in the series. Here, Chrome is the clear winner in terms of retaining high FPS even as large number of points are introduced. Its performance also stays the smoothest throughout. Using a 10ms update interval seems to cause problems for Firefox's canvas implementation. I suspect it's allowing un-presented frames to queue up and then dumping them sporadically, creating a stuttering effect, which is something we'll have to look into as we progress toward RTM. For some of the canvas implementations, it may make sense to decouple and cap the framerate to avoid overdrawing scenarios. IE9 currently seems to have some performance problems when doing large amounts of data processing and array manipulation which I believe contributes to it having lower frame rates in the above. But all the browsers perform admirably even when tasked with doing quite a lot of data processing every 10ms. As a note, the chart doesn't have a special way of interpreting these sliding window semantics, so, we could achieve even better numbers for this scenario if we introduced a series type that assumed these sliding window semantics for the data changes. Data Animation with the Infragistics Motion FrameworkTM In the samples for NetAdvantage for jQuery v11.2 we shipped a sample of smooth data-driven animation using the Infragistics Motion FrameworkTM. It doesn't display any framerate metrics, unfortunately, but could be altered to do so. The animation doesn't try to render greater than 30fps, so, in this case, that's the max value and you can see how smoothly it performs on all browsers at this link: https://www.igniteui.com/data-chart/motion-framework Let's push it harder, shall we? Let's build a sample based on the high frequency real-time sample above that will render 3 series. A column and two line series, complete with markers. We'll add some customization controls so that we can test various parameters of the setup to see how the chart does. First follow the steps from one of my previous posts to setup a project with the script and theme files for the chart. Now add this content to a web page: <script type="text/javascript"> $(function () { var currData = null, doGeneration = null, startTime = null, doUpdate = null, started = false, intervalId = 0, toggleFeed = null, curr1 = 10, curr2 = 10, curr3 = 10, i = 0, refreshCount = 0, getNextValue, num = 20.0, changeFactor = (2.0 / (num / 100.0)), markersOn = false, columnsOn = false; getNextValue = function (prevValue) { if (Math.random() > .5) { prevValue += Math.random() * changeFactor; } else { prevValue -= Math.random() * changeFactor; } if (prevValue < 1) { prevValue = 1; prevValue += Math.random() * changeFactor; } if (prevValue > 20) { prevValue = 20; prevValue += Math.random() * changeFactor; } return prevValue; } doGeneration = function () { var data = []; num = parseInt($("#volumeText").text()); changeFactor = (2.0 / (num / 100.0)); for (i = 0; i < num; i++) { curr1 = getNextValue(curr1); curr2 = getNextValue(curr2); curr3 = getNextValue(curr3); data[i] = { Label: i.toString(), Value: curr1, Value2: curr2, Value3: curr3 }; } currData = data; } doGeneration(); $("#chart1").igDataChart({ width: "670px", height: "400px", dataSource: currData, axes: [{ name: "xAxis", type: "categoryX", label: "Label" }, { name: "yAxis", type: "numericY", minimumValue: 0, maximumValue: 30 }], series: [{ name: "series1", title: "Test Series", type: "line", xAxis: "xAxis", yAxis: "yAxis", valueMemberPath: "Value" }, { name: "series2", title: "Test Series", type: "line", xAxis: "xAxis", yAxis: "yAxis", valueMemberPath: "Value2" }, { name: "series3", title: "Test Series", type: "line", xAxis: "xAxis", yAxis: "yAxis", valueMemberPath: "Value3" }], horizontalZoomable: true, verticalZoomable: true, windowResponse: "immediate" }); $("#chart1").bind("igdatachartrefreshcompleted", function () { refreshCount++; }); $("#addDataPoints").click(function () { doGeneration(); $("#chart1").igDataChart({ dataSource: currData }); }); toggleFeed = function (changeButton) { var updateTicks = 33; updateTicks = parseInt($("#intervalText").text()); if (!started) { started = true; if (changeButton) { $("#startDataFeed").val('Stop Data Feed'); $("#addDataPoints").attr("disabled", "disabled"); } intervalId = window.setInterval(function () { doUpdate(); }, updateTicks); } else { started = false; if (changeButton) { $("#startDataFeed").val('Start Data Feed'); $("#addDataPoints").removeAttr("disabled"); } window.clearInterval(intervalId); } } $("#startDataFeed").click(function () { toggleFeed(true); }); $("#volumeSlider").slider({ slide: function (event, ui) { $("#volumeText").text(ui.value.toString()); }, min: 20, max: 50000, value: 5000, step: 10 }); $("#intervalSlider").slider({ slide: function (event, ui) { $("#intervalText").text(ui.value.toString()); toggleFeed(false); toggleFeed(false); }, min: 10, max: 1000, value: 30, step: 1 }); $("#resolutionSlider").slider({ slide: function (event, ui) { var newResolution = ui.value, series1MinResolution = columnsOn ? 4.0 : 1.0, series1Resolution = 1.0; series1Resolution = Math.max(series1MinResolution, newResolution); $("#resolutionText").text(newResolution.toString()); $("#chart1").igDataChart("option", "series", [{ name: "series1", resolution: series1Resolution }, { name: "series2", resolution: newResolution }, { name: "series3", resolution: newResolution }]); }, min: 1, max: 10, value: 1, step: 1 }); doUpdate = function () { curr1 = getNextValue(curr1); curr2 = getNextValue(curr2); curr3 = getNextValue(curr3); var newData = { Label: i.toString(), Value: curr1, Value2: curr2, Value3: curr3 }; i++; currData.push(newData); $("#chart1").igDataChart("notifyInsertItem", "series1", currData.length - 1, newData); var oldItem = currData.shift(); $("#chart1").igDataChart("notifyRemoveItem", "series1", 0, oldItem); } $("#toggleMarkers").click(function () { if (markersOn) { markersOn = false; $("#chart1").igDataChart("option", "series", [{ name: "series2", markerType: "none" }, { name: "series3", markerType: "none" }]); } else { markersOn = true; $("#chart1").igDataChart("option", "series", [{ name: "series2", markerType: "automatic" }, { name: "series3", markerType: "automatic" }]); } }); $("#toggleColumns").click(function () { var currResolution = $("#resolutionSlider").slider("option", "value"), series1MinResolution = 1.0, series1Resolution = 1.0; if (columnsOn) { columnsOn = false; series1MinResolution = 1.0; series1Resolution = Math.max(series1MinResolution, currResolution); $("#chart1").igDataChart("option", "series", [{ name: "series1", remove: true }]); $("#chart1").igDataChart("option", "series", [{ name: "series1", title: "Test Series", type: "line", xAxis: "xAxis", yAxis: "yAxis", valueMemberPath: "Value", showTooltip: true, tooltipTemplate: "tooltipTemplate", resolution: series1Resolution }]); } else { columnsOn = true; series1MinResolution = 4.0; series1Resolution = Math.max(series1MinResolution, currResolution); $("#chart1").igDataChart("option", "series", [{ name: "series1", remove: true }]); $("#chart1").igDataChart("option", "series", [{ name: "series1", title: "Test Series", type: "column", xAxis: "xAxis", yAxis: "yAxis", valueMemberPath: "Value", showTooltip: true, tooltipTemplate: "tooltipTemplate", resolution: series1Resolution }]); } }); window.setInterval(function () { var refreshesPerSecond = refreshCount / 2; refreshCount = 0; $("#refreshCount").text(refreshesPerSecond); }, 2000); }); </script> <style type="text/css"> .slider { width: 200px; height: 10px; margin: 10px; } </style> </head> <body> <div class="page"> <div class="header"> <div class="title"> <h1> Performance demo</h1> </div> <div class="clear"> </div> </div> <div class="main"> <p> Below is a demo of updating 3 series simultaneously at high frequency for the Infragistics NetAdvantage jQuery Chart. You can adjust various settings to see how they affect performance. </p> <input type="button" id="addDataPoints" value="Add Data Points" /><br /> Data Points: <span id="volumeText">20</span><br /> <div id="volumeSlider" class="slider"></div> Refresh Time: <span id="intervalText">10</span><br /> <div id="intervalSlider" class="slider"></div><br /> <input type="button" id="startDataFeed" value="Start Data Feed" /><br /> <input type="button" id="toggleMarkers" value="Toggle Markers" /><br /> <input type="button" id="toggleColumns" value="Toggle Columns" /><br /> Resolution: <span id="resolutionText">1</span><br /> <div id="resolutionSlider" class="slider"></div><br /> Refreshes Per Second: <span id="refreshCount">0</span><br /> <div id="chart1"></div> </div> </body> I won't go into the details of the setup here as I've discussed this at length in my recent posts listed above. Please visit them for details on chart setup. A general sketch of the above is that we are setting up a chart with 3 different series types and setting up so that we can update the data assigned to the chart on an interval. We are also setting up a slider to control how much data is in the chart, and buttons to let us toggle the markers and whether one of the series is a column series or a third line series. Column series and markers are not especially well optimized in the CTP so they are in there to stress the performance more, but I'm allowing for them to be turned off so that we can see what the performance is like without them. Lastly we have a slider the adjusts the resolution property of the series in the chart. Raising this value will decrease the graphical fidelity of the chart but increase performance by more aggressively simplifying the geometry of the series. Increasing the resolution can be used to increase the framerate in scenarios like this even if the number of series and number of datapoints would otherwise be crippling performance. We are locking the minimum resolution of the column series to 4 (the default for column series) because if you are drawing a column every pixel, when there is that much data, it will cease to look like a column series at all and it would have been much more efficient to draw an area series instead. You can visit the resulting live demo here. First let's compare how the chart does with the minimum number of data points but with various settings enabled across the browsers: 20 points Browser 3 lines 2 lines + 1 column 2 lines + 1 column and markers Chrome 59fps 44fps 42fps IE9 70fps 64fps 46fps Safari 86fps 73fps 60fps Firefox 90fps 99fps 73fps Opera 52fps 40fps 36fps As you can see all browsers are speed demons with this few amount of points. And the way we currently implement the markers and columns causes some framerate drops in some of the browsers. Notably however, Safari and Firefox don't seem to be as bothered by this additional graphical complexity and continue to burn white hot. Now let's try this again with the maximum number of points we have the sample configured for: 50,000 points Browser 3 lines 3 lines and Resolution = 2 3 lines and Resolution = 3 2 lines + 1 column 2 lines + 1 column and markers Chrome 22fps 28fps 30fps 20fps 16fps IE9 11fps 17fps 21fps 11fps 7fps Safari 20fps 35fps 40fps 18fps 13fps Firefox 19fps 25fps 24fps 12fps 10fps Opera 16fps 23fps 27fps 12fps 8fps The winner here is a bit of a toss up between Chrome and Safari. Safari does better with the line rendering and chrome handles the addition of the columns and the markers more gracefully. The individual columns and the markers are currently handled as individual paths in the canvas and so it appears that in some browsers we pay a higher penalty for this than others. This is something we will be looking to optimize as we move toward release. As you can see, upping the number of series to 3 affects the framerate throughput we get. We are processing 3 times the number of data points every 10 milliseconds and the graphical complexity of what we are rendering has gone up quite a bit. Rendering the markers and columns put an even greater burden on performance. We think these numbers are pretty good given the level of processing going on here (and in JavaScript!!) but we will hope to improve these framerates even further as we move to RTM. And, remember, the chart doesn't have a special case for this sliding window semantic, so its actually doing more work than it technically needs to for this special case scenario. You can also see here, how, as you raise the resolution parameter and we more aggressively simplify the paths that are rendered, it seriously improves the framerate. As we move the resolution toward 3 for the 3 lines we start to approach the performance that we had for 1 line with the same number of points. Here, you can play with the live demo of this sample. Notice how the resolution parameter also helps the rendering of the column and markers too, as it also simplifies the geometry in those cases. We think the performance of our chart rocks and are excited to share it with you! Look for updates on this front as we move to RTM! -Graham