Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance Enhancements #29

Open
AlBirdie opened this issue Apr 14, 2015 · 44 comments
Open

Performance Enhancements #29

AlBirdie opened this issue Apr 14, 2015 · 44 comments

Comments

@AlBirdie
Copy link
Contributor

Are there any plans to enhance the performance of the library in a way Philip did with MPAndroidCharts?
With his enhancements it is now possible to render thousands of data points smoothly on Android.
I've had a quick glance at the ios-charts implementation and from what I've seen it is based on the original MPAndroidCharts implementation without the latest performance enhancements.

Having it render thousands of points as well would be HUGE plus for the library as it would effectively render all commercial versions useless (most of which are a waste of time and money anyways).

@danielgindi
Copy link
Collaborator

The original performance bottleneck in the Android version was many extra allocations (of arrays) inside the rendering code. Memory should not be allocated inside rendering code. So @PhilJay has moved that code into a Buffer that is preallocated, and delegating the rendering calculations to the Buffer classes while rendering.

I chose to not move the rendering calculations to the Buffer classes, but just preallocate the required memory the same way - but have the rendering calculations in the same loop as the rendering code. This way it is far easier to manage the code.

Also it IS a performance gain that the rendering calculations are not being done inside extra functions, and the data is looped only once inside the rendering code instead of twice.

Btw, in Swift performance is FAR FAR better than Java anyway, so you would have seen that performance gain even without pre-allocating those arrays.

In order to graphically explain it, this is what is/was being done in the rendering code:

  • [BEGIN RENDERING]
  • Loop over DataSets
  • -- Allocate array for points of this DataSet -- Now preallocated in both platforms
  • -- Calculate points for rendering -- Moved to Buffers in Android version
  • -- Render those points
  • [COMMIT RENDERING]

Also note that in Java, abstractions (functions, classes, inheritance) come with a great price. It is not for nothing the Google themselves wrote an important recommendation:

Be careful with code abstractions

Often, developers use abstractions simply as a "good programming practice," 
because abstractions can improve code flexibility and maintenance. 
However, abstractions come at a significant cost: 
    generally they require a fair amount more code that needs to be executed, 
requiring more time and more RAM for that code to be mapped into memory. 
So if your abstractions aren't supplying a significant benefit, you should avoid them.

Summing it up:

  • You can be comfortable with working with thousands of data points!
  • I'm trying to convince Phil to move point calculations back to rendering loops to keep it simpler to maintain ;-)

@AlBirdie
Copy link
Contributor Author

Thanks for your explanations Daniel. I'm not quite sure what to take from this, though.
What I've done is setup the most basic project (see the code below) and the performance isn't comparable with the Android version, not even close to be completely honest. Using a thousand datapoints results in massive framedrops once you get close to displaying the maximum amount of datapoints (zoom out completely) even on most current iOS devices such as iPad Mini3 and iPhone6. As I said, MPAndroidcharts can renderer several thousands on much slower devices without breaking a sweat.

override func viewDidLoad() {
super.viewDidLoad()

    lineChart = LineChartView(frame: view.frame);
    view.addSubview(lineChart);
    lineChart.backgroundColor = UIColor.whiteColor()
    lineChart.descriptionText = "Just a test"
    lineChart.leftAxis.enabled = false
    lineChart.legend.enabled = false

    setData(20)
}

func setData(range:Float) {

    var count = 1000;
    var xVals = Array<String>();

    for(var i = 0; i<count; i++) {
        xVals.append(String(i) + "");
    }

    var yVals = Array<ChartDataEntry>();

    for (var i = 0; i<count; i++) {
        var mult = range + 1
        var val:Float = Float(random()) * mult + 3;
        yVals.append(ChartDataEntry(value: val, xIndex: i));
    }

    var lineSet:LineChartDataSet = LineChartDataSet(yVals: yVals, label: " ");
    lineSet.drawCirclesEnabled = false;
    lineSet.drawValuesEnabled = false;

    var lineData:LineChartData = LineChartData(xVals: xVals, dataSet: lineSet);
    lineChart.data = lineData;
}

@danielgindi
Copy link
Collaborator

Well I haven't tested all of the charts for performance yet - so I need to do that soon.
The bottleneck is probably an allocation I have missed somewhere... I'll test and let you know! :-)

@AlBirdie
Copy link
Contributor Author

Thanks a lot Daniel. Looking forward to what you can squeeze out of this. Would be awesome to have a viable companion for MPAndroidCharts on iOS.

@AlBirdie
Copy link
Contributor Author

Well, I've played a little with the LineChartRenderer (which is particularly slow compared to for example the BarChartRenderer) and Instruments says that the CGContext's drawPath is the culprit here.
I tried mitigating this issue by using UIBezierPath instead, but performance is the same as soon as you call stroke() on the path. Before that, the drawing performance is great, but the path is filled with regards to the start and end point, not what we want for a simple line chart.
Hopefully you've got a little more luck finding a solution to this.

@danielgindi danielgindi reopened this Apr 16, 2015
@danielgindi
Copy link
Collaborator

I'm testing this right now, and yes it seems that the actual CGContextStrokePath suffers from poor performance.
One of the major hitters is the dashed line. When disabling it, the performance ~doubles. But it is still poor with a thousand points.
I'm doing some reading in Apple docs to find out how to make CoreGraphics faster

@danielgindi
Copy link
Collaborator

Yes, Core Graphics definitely is rendering the paths slow. It seems to happen because of the resolution - the resolution on newer Apple devices are very high, which means more pixels to render.

I think the CG is rendering on the CPU instead of on the GPU. Maybe someone knows a way to change that?

@AlBirdie
Copy link
Contributor Author

CG is most definitely using the CPU for rendering and as far as I know there's no way to change that.
GPU rendering would mean using OpenGL, unfortunately. Bummer that CG is that slow compared to how Android renders this stuff on the CPU.

@danielgindi
Copy link
Collaborator

danielgindi commented Apr 17, 2015 via email

@AlBirdie
Copy link
Contributor Author

There might be a chance with using the UIBezierPath. As I said, that thing is sufficiently fast as soon as you don't call 'stroke', which essentially tells the system that the path is a stroke and shouldn't be filled from the startpoint to the endpoint. I have no idea what is going on with that in the background, but this shows that the graph drawing itself isn't the issue by itself.

However, since I couldn't find a way to use the UIBezierPath properly for the line chart, it might be a viable candiate for the grid lines as one could trick the path to draw without calling stroke. Even though that is 'wrong' from an API point of view, it should yield better performance.

@danielgindi
Copy link
Collaborator

The UIBezierPath is just a UIKit wrapper around CGPath, CGContextDrawPath,
etc.
The difference in performance that is observed in some cases is due to it's
setting up the CG first, with the UIBezierPath's properties of
antialiasing, miter limit etc.

Actually there's absolutely no reason to use it when you know how to use
Core Graphics.

And yes, we do need to call "stroke", as we do want to stroke that path...
not to fill it.
Stroking in most cases is harder for the system than filling in solid
color, because it needs to take care of the width of the lines, the joins
between lines and the shape of the joins.

On Fri, Apr 17, 2015 at 10:50 AM, AlBirdie notifications@github.com wrote:

There might be a chance with using the UIBezierPath. As I said, that thing
is sufficiently fast as soon as you don't call 'stroke', which essentially
tells the system that the path is a stroke and shouldn't be filled from the
startpoint to the endpoint. I have no idea what is going on with that in
the background, but this shows that the graph drawing itself isn't the
issue by itself.

However, since I couldn't find a way to use the UIBezierPath properly for
the line chart, it might be a viable candiate for the grid lines as one
could trick the path to draw without calling stroke. Even though that is
'wrong' from an API point of view, it should yield better performance.


Reply to this email directly or view it on GitHub
#29 (comment)
.

@liuxuan30
Copy link
Member

@danielgindi have you thought about that if the animation is slowing things down, how about draw all lines without animation, and use a mask view covering it, then animate the mask view to imitate the animation?

@danielgindi
Copy link
Collaborator

Well that's a great idea! :)

Although there's a downside:
It will cripple the kind of animations you can do with it. as we allow
animating the Y axis so the chart "grows", and the way that the line fills
on the X axis is also different than when there's just a stroke.

‏My list of things to try are:

  1. Somehow enhance CoreGraphics or elevate part of the rendering to the
    GPU
    1. Pre-render all animation frames- it will delay animation start, and
      consume a considerable amount of memory
    2. Your masking idea- which limits the kinds of animations

If there are other ideas I'd love to hear them!

@AlBirdie
Copy link
Contributor Author

Daniel, did you try offloading work to the GPU yet?
I'm really, really unhappy with the commercial solution we are currently working with (terrible API, awefully closed, and loads of bugs that cause the app to crash), and having such great success with MPAndroidCharts, we'd love to switch to iOS-charts eventually if the performance is on par. Rest assured, your work would be rewarded. ;)

@danielgindi
Copy link
Collaborator

Trying a few different techniques, I'll give you an update in a few days!

On Mon, Apr 20, 2015 at 12:38 PM, AlBirdie notifications@github.com wrote:

Daniel, did you try offloading work to the GPU yet?
I'm really, really unhappy with the commercial solution we are currently
working with (terrible API, awefully closed, and loads of bugs that cause
the app to crash), and having such great success with MPAndroidCharts, we'd
love to switch to iOS-charts eventually if the performance is on par. Rest
assured, your work would be rewarded. ;)


Reply to this email directly or view it on GitHub
#29 (comment)
.

@liuxuan30
Copy link
Member

@AlBirdie what kind of performance bottleneck you met? I am currently ownning a product that draw charts just like ios-charts, we already had a chart library internally. I also concerns about the performance, currently, we just load 100-1000 data sets, seems ok now.

I am also considering changing to ios-charts if possible in the future, but our library had gestures that might have conflicts with ios-charts.

@danielgindi
Copy link
Collaborator

The performance trouble is a slow frame-rate in animations when having to
draw 500-1000 lines in a line chart.

Regarding Gestures - we are using standard UIGestureRecognizers - which you
can disable, modify, or work with. Everything is standardized. :-)

On Mon, Apr 20, 2015 at 12:53 PM, Xuan notifications@github.com wrote:

@AlBirdie https://github.com/AlBirdie what kind of performance
bottleneck you met? I am currently ownning a product that draw charts just
like ios-charts, we already had a chart library internally. I also concerns
about the performance, currently, we just load 100-1000 data sets, seems ok
now.

I am also considering changing to ios-charts if possible in the future,
but our library had gestures that might have conflicts with ios-charts.


Reply to this email directly or view it on GitHub
#29 (comment)
.

@liuxuan30
Copy link
Member

@danielgindi well, I think we use the mask view to overcome the animations... Our line chart has gradient layer. As your demo, your animation can do both X+Y direction at the same time, while we just do the X direction. I am not sure if the mask trick can help you.
What I have in my mind now is that if we had a vector to describe your X+Y direction, maybe there is a chance to use the mask trick... look forward to your results!

@AlBirdie
Copy link
Contributor Author

@liuxuan30 just general slow performance I guess. Animations aren't an issue since I'm not working with those. I'm working on finance charts where you need to have multiple datasets in a single chart (multiple stocks + a range of indicators). For a data range of 250 items that easily adds up to several thousand points that need to be rendered at once during panning and pinching. The commercial solution I'm currently working with does that pretty well (using OpenGL you can render thousands of points without overloading the CPU), but I'm not a friend of closed source libraries where you have to wait months for bug fixes.
I'd feel much more comfortable with ios-charts, especially since the API of MPAndroidCharts is just incredibly easy to work with.

@liuxuan30
Copy link
Member

I see, finance data is disaster. Our server force to just send up to 1000 data sets to the mobile device, so reduced our overload. Is there any chance to use OpenGL for ios-charts? @danielgindi

@AlBirdie
Copy link
Contributor Author

OpenGL support would be aces. A GPU rendered line chart would probably suffice for now.
Unfortunately, I've got absolutely no idea about OpenGL, otherwise I'd be happy to help out.

@danielgindi
Copy link
Collaborator

There's an improvement with CGContextStrokeLineSegments as opposed to using paths, so you should try this.
Still trying the other ways of improving things :-)

@AlBirdie
Copy link
Contributor Author

Thanks Daniel, I'll create a small test app that benches the two version against each other to see what's what. What performance gains did you gain using the new LineSegments?

@danielgindi
Copy link
Collaborator

@AlBirdie do you feel the difference with line segments?

Also I've just realized: In android to get the maximum performance you set it to hardware layer- in which case the dashed lines aren't dashed, they're all solid.

So first thing if you disable dashed lines on iOS you also get a significant boost!

But I think I can actually draw it using a OpenGL ES on CIImage, but need to be very careful because if one GL line runs when app is inactive- it will crash.

Also there's a chance I can still allow dashes by pre-rendering a texture. It's gonna take some work and it's not my main priority, but I'm starting a side project of a OpenGL layer that can seamlessly replace the CGContext.

@onlyforart
Copy link

For years in my day job we've used a commercial product that as noted above by @AlBirdie makes extensive use of OpenGL, and thus bypasses CoreGraphics for everything except annotations. Certainly they seem to be able to cope with pretty large datasets with fairly smooth rendering and they claim to use the GPU for this. However their approach comes with massive headaches, not least bugs they seemingly cannot be bothered to fix such as async rendering calls that crash in OpenGL as the app has gone into the background, idiotic class structures and insane restrictions on customisability. What I want doesn't exist today, but in iOS-Charts I see that it might be the way to get there. Anyway, for me reliability wins out over performance every time, but it's by the tiniest margin, both are massively important.

@onlyforart
Copy link

ps in my opinion dashed lines are a hangover from the days when computers didn't have the option to render in colour or even grayscale - they are not needed unless we're rendering to pure black and white and making something that looks like an old letterpress-printed textbook, surely? ;) So it's far less important to optimise for those than it is to optimise for the general 10K points case.

@AlBirdie
Copy link
Contributor Author

Absolutely with you @onlyforart regarding the dashed lines.
Given your description about your commercial charting library, I'm wondering if we've been using the same product. :) We finally ditched it by the way. What took me the better part of three months to implement (even that wasn't long enough to get around severe bugs and restrictions (e.g crosshair), took a mere two weeks with iOSCharts ('idiotic class structures' ;)). Total waste of time and money.

@onlyforart
Copy link

@AlBirdie yes that's the one. Don't even mention the crosshairs. Made with the help of too much Newcastle Brown Ale and undoubtedly aimed at the corporate market (where a gnarly API is actually a positive sales factor, because it locks customers into an expensive support/maintenance cycle). Did you know the history of those guys? actually (in their deep past) they were real pioneers in the software industry. Anyway it's time to move on and look to the future now.
We mostly draw Candlestick charts (these are financial sector apps). What I really want for iOS and Android is something that is actually very easy with HighCharts/HighStock in the HTML5/Javascript world:

  1. huge numbers of datapoints (10K or more)
  2. automatic consolidation of OHLC data into sensible scaled bars (eg show 1h bars when the zoom factor is appropriate, or 1m or 1s bars when zoomed out)
  3. smooth panning and scrolling
  4. pinch to zoom
  5. overlaying multiple data series and annotations and crosshairs, some programatically and some by user interaction eg to set a limit price, all rendered smoothly
  6. (here's the kicker - HighStock doesn't help with this! though it seems pretty obvious to me) incremental fetching of data (with lookahead) eg fetch low resolution data for the entire dataset and, as with maps apps, fetching the hi-rez data (eg millisecond "tick" data) only for the viewed area of the chart
    We're not using iOS-Charts yet though it's on my "to-do" list and getting closer to the top, so I did not look inside the code all that much yet, but as soon as I do - probably next month - I'll look into all that.
    Back to rendering, OpenGL's tendency to crash if the circumstances are less than perfect is a worry. GLKit could help with GPU drawing; I had also wondered a while back if SpriteKit could help (chart rendering isn't a million miles from making games), but right now GLKit seems a better candidate.

@AlBirdie
Copy link
Contributor Author

LOL @onlyforart , you had me at Newcastle Brown Ale. 👍 :)
I'm in the same boat regarding your requirements (working on finance products as well), but yes, let's get back to the actual drawing of these charts. Looking forward to your findings in the future.

@danielgindi
Copy link
Collaborator

@onlyforart , @AlBirdie thanks for your insights :-)

It's really nice to see people ditching commercial enterprise product for our library, although I feel bad for them... I'm conflicted!

I was hesitant to let OpenGL have a slice of this because I knew there are possible crashes if it's not managed perfectly, and that's not really possible in a UIKit app. If you're using OpenGL to create a game, the whole thing is an OpenGL canvas and you do not have to worry about a UIKit call trying to cause an OpenGL rendering while in the background.

@onlyforart your point about dashed lines is correct, but unfortunately I have had experience with clients demanding to have dashed lines. There was a case on Android where to boost performance the developer had to change the layer to a hardware layer, and told the client that dashed lines will become solid lines, that's the cost. And there's of course an option to move all drawing code to OpenGL, but then drawing a single line is a headache, but you can create a texture for a dashed line and work with it. They didn't want to pay for it so dashed lines were certainly not so critical, but they did make a a lot of noise about it.

A note to myself: Try to use GLKit for the rendering. See what happens.

And if I take your list:

  1. "huge numbers of datapoints (10K or more)" - we are working on it :) I can't say about Android, as moving it to GL will be an even bigger PITA, but it is already having pretty good performance by leveraging the hardware layer, which is doing all drawing on the GPU.
  2. "automatic consolidation of OHLC data into sensible scaled bars" - we are going to put the approximation filters into use very soon, so I think that will take care of that. Instead of having many 1m bars that you can't read, you'll have an approximation of 1 hour bar etc.
  3. "smooth panning and scrolling" - done :-)
  4. "pinch to zoom" - done :-)
  5. "overlaying multiple data series and annotations and crosshairs" - I do not know if I understand correctly, but currently there's the Combined chart that can overlay multiple chart types, and every chart type can accept multiple datasets. In addition, you can always hook into ViewPortHandler and take coordinates so you can overlay whatever you want on top of the char
  6. incremental fetching of data - we are planning on abstracting the datasource, while still having a the old APIs to set static datasets (it will just be a built in datasource that works with those). I guess that this will also fit your requirement, but we don't know when we'll get to it yet

@AlBirdie
Copy link
Contributor Author

@danielgindi , don't feel bad for them. If the product you're developing is basically a rotten tomato and yet you still charge some serious dough for it, people will eventually move to a different product. There's nothing wrong with that. I've been developing finance charts for several years now, have used pretty much all the solutions for iOS that are currently out there, and have in fact written my own charting engine in ActionScript3 back in the days, so I feel pretty confident saying that iOSCharts and MPAndroidCharts are in fact the only products I can currently recommend to any developer who needs charting in his app. Everything else out there just doesn't cut it.

Regarding the second requirement, if you guys are going to implement this, it has to be optional. We've got this kind of consolidation in one of our charting products and customers are steadily moving to a fixed frequency instead. Changing frequencies during zooming turned out to be not only confusing to the average finance charts user, power users got annoyed because they wanted fixed frequencies.

@danielgindi
Copy link
Collaborator

We've never imagined forcing this on the users! You can see that the code already includes the Filters, and in MPAndroidCharts you can see the historically the filters were enabled by a property that sets the filter (any custom filter or the built in ones), but removed later due to structural changes.

When we implement it the functionality will stay the same - it will be just another cool feature :-)

@onlyforart
Copy link

@AlBirdie Re "Regarding the second requirement, if you guys are going to implement this, it has to be optional. We've got this kind of consolidation in one of our charting products and customers are steadily moving to a fixed frequency instead. Changing frequencies during zooming turned out to be not only confusing to the average finance charts user, power users got annoyed because they wanted fixed frequencies." Completely agree - this is for charts for informational/non-trading users. Trading users have different needs.

@danielgindi Re "abstracting the datasource" maybe this is something we could contribute to, if we have time. No promises, but I'll add it to our backlog.

@AlBirdie
Copy link
Contributor Author

Did anybody play around with splitting the drawing code to separate threads (CALayer.drawsAsynchronously)? That might help in case this allows to draw chart extras, each dataset, the grid and axes separately. Given my very humble experience with CoreGraphics (read; none whatsoever ;-)), I haven't done any experiments with this, I just found it when I was looking for GLKit and how it could improve the chart performance.

@dorsoft
Copy link

dorsoft commented May 26, 2015

I needed to show a candle stick chart with 10k data points so I ran some time measurements on CandleStickChartRenderer.drawDataSet().

It turned out that most of the time is spent on calling dataSet.entryIndex (lines 76,77).

I might be mistaken, but it looks like the dataSet.entryIndex() call is redundant as _minX, _maxX values are always equal to the minx, maxx returned from dataSet.entryIndex()

I've managed to make a candle stick chart with 10k data points pan/zoom smoothly. by changing
CandleStickChartRenderer.swift, line 76,77 from:

var minx = max(dataSet.entryIndex(entry: entryFrom, isEqual: true), 0);
var maxx = min(dataSet.entryIndex(entry: entryTo, isEqual: true) + 1, entries.count);

to

var minx = max(_minX, 0);
var maxx = _maxX + 1;

I've made the same change to LineChartRenderer and was able to show a combined candle stick/line chart with 2 data series (10k data points each)

@AlBirdie
Copy link
Contributor Author

Wow, that's a massive performance increase @dorsoft !

I've just tested this and I couldn't believe my eyes. Even on an iPad 2 with 4 CombinedCharts showing up to three datasets each with 250 datapoints each and automatic min/max calculations of the y axis we are now able to pan and zoom all charts simultaneously in a fairly smooth manner. It is not 60fps, but close. Impressive for such an old device and WAY (!) faster than the commercial OpenGL solution we've talked about earlier.

@danielgindi
Copy link
Collaborator

danielgindi commented May 26, 2015 via email

@PhilJay
Copy link
Collaborator

PhilJay commented May 26, 2015

I just read this, looks very interesting but it's gonna need some intense testing to see if it is actually suitable for all scenarios :-)

@dorsoft
Copy link

dorsoft commented Jul 8, 2015

I've created a PR with candle chart performance enhancement.
The enhancment was tested thouroughly using auto scale min/max and nil values.

@codingspark
Copy link

I've found a possible bottleneck. In a realtime chart Adding many entries per second to the chart I noticed that automatically the calcMinMax of the dataset is called just after EACH values.append(e)

calcMinMax function determine the min and max values using .forEach that is causing the CPU over 60% spent looping the values, moreover in the main thread.

I'm performing some experiment disabling min/max calculation or using native for loop instead of array .forEach

@danielgindi please have a look, is really calcMinMax on all values needed? I calculate min/max Y values manually in my code and only on visible values, so maybe you could expose some boolean to enable/disable min/max auto calculation and improving performance removing the forEach function and avoid function calls.

```
open override func calcMinMax()
{
    guard !values.isEmpty else { return }

    _yMax = -Double.greatestFiniteMagnitude
    _yMin = Double.greatestFiniteMagnitude
    _xMax = -Double.greatestFiniteMagnitude
    _xMin = Double.greatestFiniteMagnitude

    values.forEach { calcMinMax(entry: $0) }
}

@jjatie
Copy link
Collaborator

jjatie commented Jan 9, 2018

@samueleperricone Please make this a separate ticket and I will prioritize this.

@codingspark
Copy link

@jjatie Thanks, Just open #3166

@uday007
Copy link

uday007 commented Feb 12, 2019

I am loading more than 13000 records in line charts for iOS. But, charts freeze the UI while loading. Also, after loading If the user selects any point then also it takes too much time to highlight the selection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants