Great problem with radchart performance

9 posts, 0 answers
  1. Pablo
    Pablo avatar
    38 posts
    Member since:
    Oct 2012

    Posted 08 Sep 2011 Link to this post

    Hello all, 

    we are having a big problem with radchart performance. I'll explain  an example to illustrate the situation. 

    In one part of our app, a user control that contains 5 Radcharts is loaded. Each chart contains two series for a total of 10, and each series has exactly 2880 points in our database. Internally, each chart is set to use a sampling threshold of 200, X axis range is set to DateTime and AutoRange, and zoom settings are set to zoom and scroll. As these are multi-series charts, the schema we used is that found in Telerik's documentation (binding to nested collections), associating each series mapping to one collection index. 

    As the user refreshes, one WCF call is asynchronously made to retrieve each series data (thus, 10 calls in total). The call returns really fast, and in around 2 seconds all the lists of points that feed the series are loaded in memory.

    The problem arises when updating the charts's ItemsSource property. It takes a lot of time since the ItemsSource of each chart is updated until all the charts are shown in the screen properly (around 15-20 seconds!!). This is totally a mess for us, as our customer needs to change the date queries dinamycally and perform refresh operations frequently. Waiting for 20-25 seconds every time a refresh is made is not acceptable for him. 

    Showing point marks, labels and so on is disabled, as well as animations. Please, could you provide any help for this?

    I post the chart configuration code here:

    // Local variables
                bool bResult = true;
                TimeSpan DateDifference = new TimeSpan();
     
                try
                {
                    // Sampling threshold
                    this.Chart.SamplingSettings.SamplingThreshold = 200;
                    this.Chart.SamplingSettings.SamplingFunction = ChartSamplingFunction.Min;
                     
                    // Settings for X axis --> manual setting for min, max, step, label step
                    this.Chart.DefaultView.ChartArea.AxisX.IsDateTime = true;
     
                    // Note: set to true !!!!
                    this.Chart.DefaultView.ChartArea.AxisX.AutoRange = true;
                    this.Chart.DefaultView.ChartArea.AxisX.TicksDistance = 15;
     
                    // Set the label format depending on the selected timespan
                    DateDifference = this.m_dtDateTo.Subtract(this.m_dtDateFrom);
     
                    TimeSpan FifteenMinutes = new TimeSpan(0, 15, 0);
                    TimeSpan OneDay = new TimeSpan(1, 0, 0, 0);
                    TimeSpan OneMonth = new TimeSpan(31, 0, 0, 0);
                    TimeSpan OneYear = new TimeSpan(365, 0, 0, 0);
     
                    // Less than 15 minutes
                    if (DateDifference.TotalMilliseconds < FifteenMinutes.TotalMilliseconds)
                    {
                        // Hours, minutes and seconds
                        this.Chart.DefaultView.ChartArea.AxisX.DefaultLabelFormat = "HH:mm:ss";
                    }
                    // Between 15 minutes and 1 day
                    else if ((DateDifference.TotalMilliseconds > FifteenMinutes.TotalMilliseconds) && (DateDifference.TotalMilliseconds < OneDay.TotalMilliseconds))
                    {
                        // Hours and minutes
                        this.Chart.DefaultView.ChartArea.AxisX.DefaultLabelFormat = "HH:mm";
                    }
                    // Between 1 day and 31 days
                    else if ((DateDifference.Days >= OneDay.Days) && (DateDifference.Days <= OneMonth.Days))
                    {
                        // Show the day
                        this.Chart.DefaultView.ChartArea.AxisX.DefaultLabelFormat = "M-dd";
                         
                        // No auto range... the chart does not handle this properly in some cases
                        this.Chart.DefaultView.ChartArea.AxisX.AutoRange = false;
                        this.Chart.DefaultView.ChartArea.AxisX.MinValue = this.m_dtDateFrom.ToOADate();
                        this.Chart.DefaultView.ChartArea.AxisX.MaxValue = new DateTime(this.m_dtDateTo.Year, this.m_dtDateTo.Month, this.m_dtDateTo.Day, 0, 0, 0).ToOADate();
                        DateTime aux1 = new DateTime(2000, 1, 1, 0, 0, 0);
                        DateTime aux2 = new DateTime(2000, 1, 2, 0, 0, 0);
                        double Step = aux2.ToOADate() - aux1.ToOADate();
                        this.Chart.DefaultView.ChartArea.AxisX.Step = Step;
                        this.Chart.DefaultView.ChartArea.AxisX.LabelStep = 1;
                    }
                    else if ((DateDifference.TotalMilliseconds > OneMonth.TotalMilliseconds) && (DateDifference.TotalMilliseconds <= OneYear.TotalMilliseconds))
                    {
                        // Month and year
                        this.Chart.DefaultView.ChartArea.AxisX.DefaultLabelFormat = "yyyy-M";
                    }
                    else if (DateDifference.TotalMilliseconds > OneYear.TotalMilliseconds)
                    {
                        // Year value
                        this.Chart.DefaultView.ChartArea.AxisX.DefaultLabelFormat = "yyyy";
                    }
     
                    // Label orientation
                    this.Chart.DefaultView.ChartArea.AxisX.LabelRotationAngle = 45;
                    this.Chart.DefaultView.ChartArea.AxisX.LayoutMode = AxisLayoutMode.Auto;
     
                    // Disable animations
                    this.Chart.DefaultView.ChartArea.EnableAnimations = false;
     
                    // X Axis label style
                    this.Chart.DefaultView.ChartArea.AxisX.AxisStyles.ItemLabelStyle = this.Resources["ItemLabelStyle"] as Style;
     
                    // Configuring the Y axis
                    this.Chart.DefaultView.ChartArea.AdditionalYAxes.Clear();
     
                    // Get the lower Y axis index
                    this.m_iLowerAxisIndex = (from axis in this.m_obcAxisY
                                              select axis.ID).Min();
                    
     
                    foreach (DLL_CustomControls.ReportChartSettings.AxisY obConfiguredAxisY in this.m_obcAxisY)
                    {
                        if (obConfiguredAxisY.ID == this.m_iLowerAxisIndex)
                        {
                            // Configuration for the default axis
                            this.Chart.DefaultView.ChartArea.AxisY.MinValue = obConfiguredAxisY.MinValue;
                            this.Chart.DefaultView.ChartArea.AxisY.MaxValue = obConfiguredAxisY.MaxValue;
                            this.Chart.DefaultView.ChartArea.AxisY.AutoRange = obConfiguredAxisY.AutoRange;
                            this.Chart.DefaultView.ChartArea.AxisY.Title = obConfiguredAxisY.Label;
                            continue;
                        }
     
                        // Create and add new axis
                        Telerik.Windows.Controls.Charting.AxisY NewAxis = new Telerik.Windows.Controls.Charting.AxisY();
                        NewAxis.MinValue = obConfiguredAxisY.MinValue;
                        NewAxis.MaxValue = obConfiguredAxisY.MaxValue;
                        NewAxis.AutoRange = obConfiguredAxisY.AutoRange;
                        NewAxis.AxisName = obConfiguredAxisY.ID.ToString();
                        NewAxis.Title = obConfiguredAxisY.Label;
     
                        // Add the secondary axis
                        this.Chart.DefaultView.ChartArea.AdditionalYAxes.Add(NewAxis);                   
                    }
                }
                catch (Exception ex)
                {
                    bResult = false;
                    ExceptionManager.Write("DLL_CustomControls", "ReportChart", "ConfigureChart", "Exception: " + ex.Message);
                }
     
                return bResult;

    And here, the function used to process the result of the asynchronous WCF call. As you can see, we make a local copy of the list returned by the WCF method inserting it in an ObservableCollection and then setting the ObservableCollection as an item of m_lstChartSource, which is a list and the object set as Chart.ItemsSource target. "ProcessChartSerie" makes additional processing regarding the serie (color, item mappings and so on):

    // Capture the data
                    if (!e.Result)
                    {
                        // Debug, warn and return
                        DebugManager.Write(DebugManager.DebugStatus.ERROR, "DLL_CustomControls", "ReportChart", "OnGetDataCompleted", "Error in the web service function for Serie ID = " + e.outSerieID.ToString() + ": " + e.strError);
                        MessageBox.Show("Error getting data for serie " + e.outSerieID.ToString() + ". Please try again.", "Error", MessageBoxButton.OK, MessageBoxImage.Exclamation);
                        return;
                    }
     
                    // Capture the serie that has been processed
                    DLL_CustomControls.ReportChartSettings.ChartSerie obSerieToProcess = null;
     
                    obSerieToProcess = (from series in this.m_obcSeries
                                       where series.ID == e.outSerieID
                                       select series).First();
     
                    // Process the serie data
                    if (!ProcessSerieData(obSerieToProcess))
                    {
                        DebugManager.Write(DebugManager.DebugStatus.ERROR, "DLL_CustomControls", "ReportChart", "OnGetDataCompleted", "Error processing serie number " + e.outSerieID.ToString());
                    }
                    else
                    {
                        // If the list has points, process
                        if (e.lstChartPoints != null)
                        {
                            // Add to list of datasources of the chart
                            ObservableCollection<DLL_Resources.WCF_Clients.ChartValueDateDouble> obcPoints = new ObservableCollection<DLL_Resources.WCF_Clients.ChartValueDateDouble>();
     
                            // Convert in observable collection
                            foreach (DLL_Resources.WCF_Clients.ChartValueDateDouble obPoint in e.lstChartPoints)
                            {
                                DLL_Resources.WCF_Clients.ChartValueDateDouble obNewPoint = new DLL_Resources.WCF_Clients.ChartValueDateDouble();
                                obNewPoint.XValueDateTime = obPoint.XValueDateTime;
                                obNewPoint.XValueDouble = obPoint.XValueDouble;
                                obNewPoint.YValue = obPoint.YValue;
     
                                // Add point
                                obcPoints.Add(obNewPoint);
                            }
     
                            this.m_lstChartSource.Add(obcPoints);
                        }
                    }



  2. Vladimir Milev
    Admin
    Vladimir Milev avatar
    1061 posts

    Posted 14 Sep 2011 Link to this post

    Hello Pablo,

    What kind of series do you use? Bar series? Line series? If you are using line or area series can you please "snoop" your application and let us know if you find any series items to be generated.

    Best wishes,
    Vladimir Milev
    the Telerik team

    Explore the entire Telerik portfolio by downloading the Ultimate Collection trial package. Get it now >>

  3. UI for WPF is Visual Studio 2017 Ready
  4. Pablo
    Pablo avatar
    38 posts
    Member since:
    Oct 2012

    Posted 14 Sep 2011 Link to this post

    Hello Vladimir, 

    Thanks for your response.

    We are using line series for all series in all charts. I don't understand what you mean about "series items to be generated"...

    I'll try to explain myself better using as an example what happens in our app when the user clicks "refresh". The following refers to a single chart, but it happens "simultaneously" to all of them, as each chart is embedded in a custom user control instance (the user control has a RadChart and a RadOutlookBar). 

    When the user clicks the "Refresh button", the chart's ItemsSource property is set to NULL (reset binding), the series mappings are cleared and the main and additional Y axis are configured in the chart. Also, the sampling threshold is set to 200:

    // First, clear the chart series mappings
    this.Chart.SeriesMappings.Clear();
     
    // Clear the data source
    this.m_lstChartSource.Clear();
    this.Chart.ItemsSource = null;

    Just to let you know, the m_lstChartSource object is a list of lists (we use the nested collections binding scheme). As no items are going to be added to or removed from the source on the fly, we don't need to use an ObservableCollection:

    private List<List<DLL_Resources.WCF_Clients.ChartValueDateDouble>> m_lstChartSource;

    After pre-configuring the Radchart, a WCF async call is launched per serie to retrieve its data. The return of the call is handled in a callback function. When a series data is received, the serie is configured (basically, a new series mapping is created and added to the chart, indicating that it is a LinearSeriesDefinition). By default, the series visibility is set to hidden. Then, the list of points received is added to the m_lstChartSource object:

    this.m_lstChartSource.Add(e.lstChartPoints);

    NOTE: An alternative to clearing the source list would be to keep the list filled and update the point values with those coming from the WCF call (using a bussiness class that implements the INotifyPropertyChanged). However, each serie has exactly 2880 points (a whole day, every 30 seconds), so looping each serie to assign its new values doesn't sound like a good idea...

    Only when all the series have been received, the chart's ItemsSource property is assigned and a "ChartCompleted" custom event is raised to notify the parent control that this chart has already obtained its data:

    if (this.m_iQueryCounter == 0)
    {
        // Set chart data source
        this.Chart.ItemsSource = this.m_lstChartSource;
     
        // Raise we are done
        this.RaiseChartCompleted();
    }

    As the parent controls receives this event, it asks the chart to make visible the series that are configured as visible (this setting is stored in an internal settings class).

    This process is done for every chart. We have been able to reduce the drawing time setting only one series visible per chart instead of all of them. This reduces the gap between the Refresh button is pressed and the charts are correctly shown to around 10-12 seconds. However... it's still to much for us. 

    I've been trying to workaround this for a few days and I haven't reached a better solution, probably due to my low knowledge about the control. My conclussion is that the bottle neck comes when the charts have to paint the series.

    I made a test setting all series to hidden and the gap between the Refresh button is pressed and the charts are shown properly (obviously nothing is painted but the axis, the title and the legend) is reduced to 2-3 seconds. Then, making the series visible/not visible is really fast (we make this with a check button that controls the series visibility).

    Sorry for the big tale... it's hard to explain the situation. Thank you in advance for your patience. 

    Best wishes, 

    Pablo.

  5. Pablo
    Pablo avatar
    38 posts
    Member since:
    Oct 2012

    Posted 14 Sep 2011 Link to this post

    Hi Vladimir,

    I think that I found a solution looking in the forum: http://www.telerik.com/community/forums/wpf/chart/how-to-increase-radchart-performance-with-thousands-of-data-points.aspx

    Now, i explicitly set this:

    LineSeriesDefinition obLine = new LineSeriesDefinition();
    AnimationSettings obAnimationSettings = new AnimationSettings();
    obAnimationSettings.ItemAnimationDuration = new TimeSpan(0);
    obAnimationSettings.ItemDelay = new TimeSpan(0);
    obAnimationSettings.TotalSeriesAnimationDuration = new TimeSpan(0);
     
    obLine.AnimationSettings = obAnimationSettings;
    obLine.ShowItemLabels = false;
    obLine.ShowItemToolTips = false;
    obLine.ShowPointMarks = false;
     
    obSerieToProcess.TelerikSeriesMapping.SeriesDefinition = obLine;

    Before, we were edditing directly the properties of SeriesDefinition, which did not allow to make ItemLabels, ItemToolTips and PointMarks not visible. This boosted performance a lot. 

    Thanks for your patience :-). Any other suggestions greatly appreciated.

    Pablo.
  6. Vladimir Milev
    Admin
    Vladimir Milev avatar
    1061 posts

    Posted 16 Sep 2011 Link to this post

    Hi Pablo,

    I was basically trying to figure out whether you have successfully set all those settings to false. The result from this configuration is that no series items are generated for series which are rendered as one-piece such as (line, area etc.). I am glad thinks worked out!

    To further increase performance you may play around with SamplingThreshold property to find the sweet spot for your data.

    Greetings,
    Vladimir Milev
    the Telerik team

    Explore the entire Telerik portfolio by downloading the Ultimate Collection trial package. Get it now >>

  7. Pablo
    Pablo avatar
    38 posts
    Member since:
    Oct 2012

    Posted 16 Sep 2011 Link to this post

    Hi Vladimir, 

    thanks for your interest. We definitely were able to do it and performance increased a lot... we are now showing the charts in less than 4 seconds, including the WCF invokes to retrieve the data. We are using a SamplingThreshold of 200 and it works great.

    By the way, I wanted to ask you about one more thing. Aside from this WPF app, we are building a Silverlight equivalent so that our customers can virtually have the same application in their desktop or in the web. We are using (of course ;-p) the Telerik components for Silverlight. However, although we have applied the same configuration for the charts, we are having a worse perfomance, I mean: while in the WPF app making the async calls for the charts + plotting them may take around 3-4 seconds, in the Silverlight counterpart the same happens in 12-15 seconds. The code is virtually the same. Is it normal to have a worse performance in Silverlight... or may be we are missing something? 

    Thank you very much for your advices. 

    Greetings,

    Pablo.
  8. Giuseppe
    Admin
    Giuseppe avatar
    2363 posts

    Posted 21 Sep 2011 Link to this post

    Hello Pablo,

    Generally, speaking about chart plotting & live data scenarios, our observations show that most of the time the Silverlight app would outperform the WPF one (all other conditions being equal). Could you confirm that the time is lost in rendering and not in async calls? If you believe the problem to be chart-related, you can open a formal support ticket and send us a sample application that we can investigate locally as well.


    Kind regards,
    Giuseppe
    the Telerik team

    Explore the entire Telerik portfolio by downloading the Ultimate Collection trial package. Get it now >>

  9. Pablo
    Pablo avatar
    38 posts
    Member since:
    Oct 2012

    Posted 21 Sep 2011 Link to this post

    Hi Guiseppe,

    we have come to the true problem this morning. It is not related to Telerik control's performance, but to the fact that the async calls are being wrapped by the browser using an Accept-Encoding = identity HTTP header instead of Accept-Encoding = gzip, deflate HTTP header. This is causing that the data sent back by the WCF service is not compressed, which explains why the delay in the response is so much compared to the WPF case. 

    As a workaround, we have implemented a manual compression of the chart data using Telerik.Windows.Zip classes, which has definitely boosted performance reducing the total time from 12-15 seconds to 3-4 seconds. 

    Do you know if there is any reason why the browsers are not sending the gzip, deflate value in that HTTP header?

    Thanks for your help. 

    Pablo.
  10. Bartholomeo Rocca
    Bartholomeo Rocca avatar
    247 posts
    Member since:
    May 2006

    Posted 26 Sep 2011 Link to this post

    Hello Pablo,

    I think WCF service and client do not support HTTP Compression out of the box in .NET 3.5. Check the following articles here and here.


    Greetings,
    Bart.
Back to Top
UI for WPF is Visual Studio 2017 Ready