0

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?

samp17
  • 547
  • 1
  • 4
  • 16
  • Unfortunately core graphics draws its graphics on the CPU. if you use metal and draw your data on the GPU it can handle much higher polygons and filtrates, but its obviously more complicated to implement. – Josh Homann Dec 12 '19 at 21:28
  • Thank you for your reply. Obviously it is a lot more complicated to implement metal, but if it's what I need to use for a responsive app, then I shall delve in. However, after much reading, I am to understand that the vertices in Metal, still need to be computed on the CPU before passed to the GPU for rendering. So in effect, for what I am doing, this isn't much different to what I am doing here, I just. need to find a way to. optimise my calcDataCoOrdinates code. – samp17 Dec 13 '19 at 07:53
  • Its not computing the vertices that's expensive; its rasterizing the lines and polygons between those vertices into millions of potentially overlapping pixels that is expensive and this is where the GPU will save you time since rasterization is inherently paralellizable. – Josh Homann Dec 13 '19 at 17:40

0 Answers0