I am writing my own code in swift to draw a graph to display a large data set. (The reason not to use existing frameworks is I would like to use logarithmic and linear views, as well as user interaction with the graph). I need help trying to speed my code to try and make the app feel responsive, as it is too slow currently.
I pass my dataset into my magnitudeGraph class (a UIView), and calculate the co-ordinates for the data line (divLine) and save them as a UIBezierPath using the function below:
func calcDataCoOrdinates(frequencies: [Float], Mag: [Float]) -> UIBezierPath {
let divLine = UIBezierPath()
let graphWidth:Int = width - dBLabelSpace
let graphHeight:Int = height - freqLabelSpace
let yRange = Float(yMax-yMin)
//Limit Data to the Values being displayed
let indexOfGreaterThanXMin = frequencies.firstIndex(where: {$0 > xMin})
let indexLessThanXMax = frequencies.lastIndex(where: {$0 < xMax})
let frequencyRange = frequencies[indexOfGreaterThanXMin!..<indexLessThanXMax!]
let MagnitudeRange = Mag[indexOfGreaterThanXMin!..<indexLessThanXMax!]
let graphRatio = (yMax-yMin)/Float(graphHeight)
let logX = vForce.log10(frequencyRange)
let logRange = log10(Double(xMax))-log10(Double(xMin));
let graphDivideByLog = Float(graphWidth) / Float(logRange)
let xMinArray:[Float] = Array(repeating: Float(log10(xMin)), count: logX.count)
let x5 = vDSP.multiply(subtraction: (a: logX, b: xMinArray), graphDivideByLog)
let x6 = vDSP.add(Float(dBLabelSpace + 10), x5)
let yMaxArray = Array(repeating:Float(yMax), count: MagnitudeRange.count)
let y2 = vDSP.subtract(yMaxArray, MagnitudeRange)
let y3 = vDSP.divide(y2, Float(graphRatio))
var allPoints:[CGPoint]=[]
for i in 0..<x6.count{
let value = CGPoint.init(x: Double(x6[i]), y: Double(y3[i]))
allPoints.append(value)
}
divLine.removeAllPoints()
divLine.move(to: allPoints[0])
for i in 1 ..< allPoints.count {
divLine.addLine(to: allPoints[i])
}
return divLine
}
Instead of calling setNeedsDisplay directly from the UI element that triggers this, I call redrawGraph(). This calls the calcDataCoOrdinates function in a background thread, and then once all coordinates are calculated, I call setNeedsDisplay on the main thread.
func redrawGraph(){
width = Int(self.frame.width);
height = Int(self.frame.height);
magnitudeQueue.sync{
if mag1.isEmpty == false {
self.data1BezierPath = self.calcData1CoOrdinates(frequencies: freq1, Mag: mag1)
}
if mag2.isEmpty == false {
self.data2BezierPath = self.calcData1CoOrdinates(frequencies: freq2, Mag: mag2)
}
if magSum.isEmpty == false {
self.dataSumBezierPath = self.calcData1CoOrdinates(frequencies: freqSum, Mag: magSum)
}
DispatchQueue.main.async {
self.setNeedsDisplay()
}
}
Finally, I post the code of actual drawing function:
override func draw(_ rect: CGRect) {
self.layer.sublayers = nil
drawData(Color: graphData1Color, layer: in1Layer, lineThickness: Float(graphDataLineWidth), line:data1BezierPath)
drawData(Color: graphData2Color, layer: in2Layer, lineThickness: Float(graphDataLineWidth), line:data2BezierPath)
drawData(Color: sumColor, layer: in3Layer, lineThickness: Float(sumLineThickness), line: dataSumBezierPath)
}
func drawData(Color: UIColor, layer: CAShapeLayer, lineThickness:Float, line:UIBezierPath){
layer.removeFromSuperlayer()
layer.path = nil
Color.set()
line.lineWidth = CGFloat(lineThickness)
layer.strokeColor = Color.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.path = line.cgPath
layer.lineWidth = CGFloat(lineThickness)
layer.contentsScale = UIScreen.main.scale
let mask = CAShapeLayer()
mask.contentsScale = UIScreen.main.scale
mask.path = bpath?.cgPath
layer.mask = mask
self.layer.addSublayer(layer)
}
This code works well to calculate the three data sets and view them on the graph. I have excluded the code to calculate the background layer (grid lines), but that is just to simplify the code.
The issue I get is that this is a lot to try and process for 'realtime' interactivity. I figure that we want around 30fps for this to feel responsive, which gives approximately 33ms of processing and rendering time in total.
Now if I have say 32k data points, currently measuring these, each of the three passes of calcDataCoOrdinates can take between. 22-36ms, totalling around 90ms. 1/10th of a second is very laggy, so my question is how to speed this up? If I measure the actual drawing process this is currently very low (1-2ms), which brings me to believe it is my calcDataCoOrdinates function that needs improving.
I have tried several approaches, including using a Douglas-Peucker algorithm to decrease the amount of data points, however this is at a large time cost (+100ms).
I have tried skipping data points that overlap pixels
I have tried to use accelerate framework as much as possible, and the top half of this function operates in approximately 10ms, however the bottom half putting the coordinates into a CGPoint array, and iterating to make a UIBezierPath takes 20ms. I have tried pre-initializing the allPoints array with CGPoints, figuring this will save some time, but this is still taking too long.
Can this be computed more. efficiently or have I hit the limit of my app?