3

I'm writing a real time graphing app using Scichart for android. I've been using

FastLineRenderableSeries as a wrapper for my data series

But I'm wondering what other techniques with Android SciChart exist in order to maximize graphing speed?

Particularly I've noticed a decrease in performance when I use IXyDataSeries and increase the x axis size to 100,000 pts from 10,000. The speed of the graphing stays consistently fast until I've added about 90,000 points to my IXyDataSeries.

Thanks guys. I'm new to stackoverflow... more of a mechE than a CS person.

Here is my graphFragment class which takes in UDP sensor data as a string, splices it and adds it to the IXyDataSeries.

public class GraphFragment extends Fragment { 

    //Various fields...
    //UDP Settings
    private UdpClient client;
    private String hostname;
    private int remotePort;
    private int localPort;

    //Use to communicate with UDPDataClass
    private Handler handler;

    private boolean listenerExists = false;
    private int xBound = 100000; //**Graphing Slows if xBound is TOO large**
    private int yBound = 5000;
    private boolean applyBeenPressed = false;

    private GraphDataSource dataSource; //Gets data from UDPDataClass
    private SciChartSurface plotSurface; //Graphing Surface
    protected final SciChartBuilder sciChartBuilder = SciChartBuilder.instance();

    //Data Series containers
    //Perhaps it would be better to use XyySeries here?
    private final IXyDataSeries<Double, Double> dataSeriesSensor1 = sciChartBuilder.newXyDataSeries(Double.class, Double.class).build();
    private final IXyDataSeries<Double, Double> dataSeriesSensor2 = sciChartBuilder.newXyDataSeries(Double.class, Double.class).build();
    private final IXyDataSeries<Double, Double> dataSeriesSensor3 = sciChartBuilder.newXyDataSeries(Double.class, Double.class).build();
    private final IXyDataSeries<Double, Double> dataSeriesSensor4 = sciChartBuilder.newXyDataSeries(Double.class, Double.class).build();
    private final IXyDataSeries<Double, Double> dataSeriesSensor5 = sciChartBuilder.newXyDataSeries(Double.class, Double.class).build();
    private final IXyDataSeries<Double, Double> dataSeriesSensor6 = sciChartBuilder.newXyDataSeries(Double.class, Double.class).build();
    private ArrayList<IXyDataSeries<Double,Double>> dataSeriesList = new ArrayList<>(Arrays.asList(dataSeriesSensor1,dataSeriesSensor2,dataSeriesSensor3,dataSeriesSensor4, dataSeriesSensor5, dataSeriesSensor6));
    private ArrayList<Double> xCounters = new ArrayList<>(Arrays.asList(0.0,0.0,0.0,0.0,0.0,0.0));

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    final View frag = inflater.inflate(R.layout.graph_fragment, container, false);

    plotSurface = (SciChartSurface) frag.findViewById(R.id.dynamic_plot);

    dataSource = new GraphDataSource(); //Run the data handling on a separate thread
    dataSource.start();

    UpdateSuspender.using(plotSurface, new Runnable() {
        @Override
        public void run() {
            final NumericAxis xAxis = sciChartBuilder.newNumericAxis().withVisibleRange(0,xBound).build();
            final NumericAxis yAxis = sciChartBuilder.newNumericAxis().withVisibleRange(0,yBound).build();

            //These are wrappers for the series we will add the data to...It contains the formatting
            final FastLineRenderableSeries rs1 = sciChartBuilder.newLineSeries().withDataSeries(dataSeriesSensor1).withStrokeStyle(ColorUtil.argb(0xFF, 0x40, 0x83, 0xB7)).build(); //Light Blue Color
            final FastLineRenderableSeries rs2 = sciChartBuilder.newLineSeries().withDataSeries(dataSeriesSensor2).withStrokeStyle(ColorUtil.argb(0xFF, 0xFF, 0xA5, 0x00)).build(); //Light Pink Color
            final FastLineRenderableSeries rs3 = sciChartBuilder.newLineSeries().withDataSeries(dataSeriesSensor3).withStrokeStyle(ColorUtil.argb(0xFF, 0xE1, 0x32, 0x19)).build(); //Orange Red Color
            final FastLineRenderableSeries rs4 = sciChartBuilder.newLineSeries().withDataSeries(dataSeriesSensor4).withStrokeStyle(ColorUtil.argb(0xFF, 0xFF, 0xFF, 0xFF)).build(); //White color
            final FastLineRenderableSeries rs5 = sciChartBuilder.newLineSeries().withDataSeries(dataSeriesSensor5).withStrokeStyle(ColorUtil.argb(0xFF, 0xFF, 0xFF, 0x99)).build(); //Light Yellow color
            final FastLineRenderableSeries rs6 = sciChartBuilder.newLineSeries().withDataSeries(dataSeriesSensor6).withStrokeStyle(ColorUtil.argb(0xFF, 0xFF, 0x99, 0x33)).build(); //Light Orange color

            Collections.addAll(plotSurface.getXAxes(), xAxis);
            Collections.addAll(plotSurface.getYAxes(), yAxis);
            Collections.addAll(plotSurface.getRenderableSeries(), rs1, rs2, rs3, rs4, rs5, rs6);
        }
    });

    return frag;
    }

 //This class receives the UDP sensor data as messages to its handler
 //Then it splices the data
 //Adds the data to the IXySeries
 //Then the UpdateSuspender updates the graph
 //New data arrives approx every 50 ms (around 20x a second)
 //Graphing slows when xAxis is increased to ~100,000
 //X data is only counters...Only care about Y data
 public class GraphDataSource extends Thread{

    public void run(){
        Looper.prepare();
        //Get Data from UDP Data Class when its available
        handler = new Handler(){
            public void handleMessage(Message msg){
                String sensorData = msg.getData().getString("data"); //Data receiveds
                if(dataValid(sensorData)){
                    sensorData = sensorData.replaceAll("\\s", "");
                    final String[] dataSplit = sensorData.split(","); //split the data at the commas

                    UpdateSuspender.using(plotSurface, new Runnable() {    //This updater graphs the values
                            @Override
                            public void run() {
                                spliceDataAndAddData(dataSplit);
                            }
                        });
                }
            }
        };
        Looper.loop();
    }

    /**
     *
     * @param data string of the udp data
     * @return true if the data isn't corrupted..aka the correct length
     */
    private boolean dataValid(String data){
        return ((data.length() == 1350));
    }

    /**
     *
     * @param dataSplit String[] of the entire data
     *  Adds the each sensor data to the IXySeries representing the data
     */
    private void spliceDataAndAddData(String[] dataSplit){
        addToSensorSeries(dataSplit, 1);
        addToSensorSeries(dataSplit, 2);
        addToSensorSeries(dataSplit, 3);
        addToSensorSeries(dataSplit, 4);
        addToSensorSeries(dataSplit, 5);
        addToSensorSeries(dataSplit, 6);
    }

    /**
     *
     * @param dataSplit data to split into individual sensor array
     *                  must contain only string representations of numbers
     * @param sensorSeriesNumber which sensors to collect the data points of
     * Adds the data to the corresponding IXySeries 
     */
    private void addToSensorSeries(String[] dataSplit, int sensorSeriesNumber){
        sensorSeriesNumber -= 1;  //Adds each value individually to the series
        double xcounter = xCounters.get(sensorSeriesNumber);
        int i = sensorSeriesNumber;
        int dataSize = dataSplit.length - 1;
        String num = "";
        while(true){
            if(i < 6){ //This is the base case...add the first set of data
                num = dataSplit[i];
                try {
                    if(xcounter > xBound){
                        xcounter = 0;
                        dataSeriesList.get(sensorSeriesNumber).clear();
                    }
                    dataSeriesList.get(sensorSeriesNumber).append(xcounter, Double.parseDouble(num)); //appends every number...
                }catch (Exception e){
                    //Corrupt data
                }
            }else if((i) <= dataSize && i >= 6){ //Will start to get hit after the second time
                num = dataSplit[i];
                try {
                    if(xcounter > xBound){
                        xcounter = 0;
                        dataSeriesList.get(sensorSeriesNumber).clear();
                    }
                    dataSeriesList.get(sensorSeriesNumber).append(xcounter, Double.parseDouble(num));
                }catch (Exception e){
                    //Corrupt data
                }
            }else{
                break;
            }
            xcounter++;
            i += 6;
        }
        xCounters.set(sensorSeriesNumber,xcounter);
    }
}
Haroon
  • 496
  • 5
  • 14
  • 31
mmellor
  • 33
  • 7
  • Usually when asking questions on stackoverflow it's customary to give a code sample so people can understand what you've already tried, what the problem may be and perhaps identify why it didn't work. – Dr. Andrew Burnett-Thompson Oct 02 '16 at 11:20
  • 1
    Hi @Dr.ABT thanks for responding. I've added a code sample above. I've tried to derive my code from the Performance demo android example. I'm using an IXyDataSeries with the FastLineRenderable Series. I'm reading data into my fragment class using the Handler structure in android and splicing the data that I receive then adding it to IXyDataSeries. After that I use the UpdateSuspender to replot the points. The graphing works well until I increase the X Axis size to 100,000 points or so and it only slows down after around 90,000 points have been added. If you have any ideas that would help alot – mmellor Oct 06 '16 at 19:27
  • It's kinda strange because we have tested the chart up to a million points or so on modern Android devices. Question: Does the Android performance demo exhibit slowdown? https://www.scichart.com/android-chart-realtime-performance-demo/ also see https://www.scichart.com/android-chart-loading-1-million-points-instantly-demo/ where you can click to add 100k or 1M points – Dr. Andrew Burnett-Thompson Oct 06 '16 at 20:02
  • ... and from the code, xBound just affects xAxis.VisibleRange right? – Dr. Andrew Burnett-Thompson Oct 06 '16 at 20:05
  • Yeah it just affects xAxis.VisibleRange. I'm using an HTC One from 2013 to do testing. The Android performance demo works splendidly on it. I wouldn't be surprised if my code was horribly inefficient <- not a programmer by nature – mmellor Oct 06 '16 at 20:11
  • I'm adding 6 * (45 data points) every ~50 ms to the graph just for reference – mmellor Oct 06 '16 at 20:23
  • xBound actually also affects the IXyDataSeries by effectively determining how many values it will hold – mmellor Oct 06 '16 at 20:50

1 Answers1

4

I took a look on you code and I'm not sure if we can do something about it. Your example contains 6 XyDataSeries with XRange from 0 to 100000 this gives 600 000 points on screen which is pretty good for realtime example on HTC One. In SciChart performance demo you can see usage of only 3 XyDataSeries instances which allows to draw more points in each series

Disclosure: I am the lead developer on the SciChart Android team


But I think you can get few extra FPS by adding some optimizations in your code. The main problem in realtime charts is in code which updates a chart - it is called very often so if you create some objects during update and don't save it then this could cause problem because of GC in Android (GC pass is slow and it can pause all application's threads while GC is collecting all unused objects). So I would suggest you to do next:

  • I would suggest to increase heap size in your aplication: more memory application has - less GC it performs if you use memory efficiently.
  • Try to reduce amount of boxing/unboxing and allocate less objects in code which is called frequntly(such as data series updates). Basically you need to forget about creation of any objects in callbacks which update data series. In your code I noticed few places where boxing/unboxing occurs. This code is called every second and some methods are called in a loop so effect of boxing/unboxing can significantly affect performance of your application:

dataSeriesList.get(sensorSeriesNumber).append(xcounter, Double.parseDouble(num));

double xcounter = xCounters.get(sensorSeriesNumber);

xCounters.set(sensorSeriesNumber,xcounter);

I would suggest you to use append override which accepts IValues. Usage of append which accepts IValues allows to avoid unnecessary boxing/unboxing of primitive types when you append alot of data very frequently.

  • Also I would suggest to use Float or Integer unless you really need Double when you create XyDataSeries. This potentially allows to reduce memory consumption by half (8 bytes to store double vs 4 bytes to store int/float) and as result application has more free memory which allows to perform GC less frequently.

Hope this will help you.

Yura Khariton
  • 484
  • 2
  • 6
  • 2
    Hi Yura! Thanks a lot! I implemented your advice and just tested the graphing for 6 sensors with 1,00,000 points each (1 Million!) and the graphing stays responsive! Thanks! I think fixing the unboxing and using the append override were what did it, but I implemented all the suggestions you made.Thanks! Scichart is amazing. :) – mmellor Oct 14 '16 at 19:46