1

The issue is now resolved. See the last edit for details!

I have an UIViewController with an audio visualisation and a button. When a button is pressed, the following function is fired:

func use(sender:UIButton!) {
    // Analyse the audio
    let analysisQueue = dispatch_queue_create("analysis", DISPATCH_QUEUE_CONCURRENT)
    dispatch_async(analysisQueue, {
        // Initialise the analysis controller
        let analysisController = AnalysisController()
        analysisController.analyseAudio(global_result.filePath, completion: {
            // When analysis is complete, navigate to another VC
            dispatch_async(dispatch_get_main_queue(), {
                let mainStoryboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
                let vc : UIViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ResultDetailViewController") as UIViewController
                let navigationController = self.navigationController
                // Pop the current VC before pushing the new one
                navigationController?.popViewControllerAnimated(false)
                navigationController?.pushViewController(vc, animated: false)
            })
        })
    })
}

Here, I create a background queue and start a very lengthy signal processing operation. Once the processing is complete, I perform navigation to another view controller using the main queue.

This causes the ResultDetailViewController to appear on the screen with all the relevant data and visualisation fully loaded.

However, for the first 2-3 seconds after the VC has been loaded, none of the buttons work! If I click any button within this initial period, the action would be fired once that initial period is over.

When I perform this transition from any other VC, the ResultDetailViewController is loaded smoothly, and everything works.

What am I doing wrong? Any help would be much appreciated!

EDIT 1

I will add more details about my set-up: In AnalysisController, I am doing the following:

  1. Process the signal using FFT's and such
  2. Update a the properties of a global_result
  3. Trigger a method of global_result, which stores the result in CoreData and uses Alamofire to POST the data to my server
  4. Once the first POST succeeds, the callback updates global_result's id, and fires few more POST requests.
  5. Completion handler is triggered, which then causes the transition

global_result, is my custom global Object which is initialised as a public var.

In theory, the completion handler should be triggered once the processing completes, results are saved in CoreData, and the first POST request is dispatched.

In ResultDetailViewController's viewDidLoad function, I am copying the global_result into a local variable and creating the UI elements using data from global_result.

Now, I suspected that the lag occurs due to background thread using global_result when ResultDetailViewController is already loaded, so I tried to create a new instance of Result class, instead of copying the global_result, but that also didn't help.

And here's the Result class:

import Foundation
import CoreData
import Alamofire
import CryptoSwift

public class Result {
    var localID: Int
    var id: Int
    var filePath: NSURL!
    var result: Double
    var origSound: [Double]


    init(localID: Int, id: Int, filePath: NSURL, result: Double, origSound: [Double]) {
        // Initialize stored properties.
        self.localID = localID
        self.id = id
        self.filePath = filePath
        self.result = result
        self.origSound = origSound
    }

    func store() {
        self.storeLocal()

        // Serialise data
        let parameters = [
            "localID": self.localID,
            "id": self.id,
            "result": self.result
        ]

        // Prepare request
        let request = NSMutableURLRequest(URL: NSURL(string: "my_server/script.php")!)
        request.HTTPMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        // Encode parameters in JSON and encrypt them
        request.HTTPBody = dictToEncryptedJSONData(rsaKey, parameters: parameters)

        // POST the data to server
        Alamofire.request(request)
            .responseJSON { response in
                if let JSON = response.result.value {
                    if let myID = JSON as? Int {

                        self.id = myID

                        // Store the global ID in CoreData
                        updateIDLocal(self.localID, id: self.id)

                        // Serialise and POST array
                        let param = [
                            "origSound": self.origSound
                        ]

                        // Prepare request
                        var request = NSMutableURLRequest(URL: NSURL(string: "my_server/script2.php?id=\(self.id)")!)
                        request.HTTPMethod = "POST"
                        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

                        // Encode parameters in JSON and encrypt them
                        request.HTTPBody = dictToEncryptedJSONData(rsaKey, parameters: param)

                        // POST the data to server
                        Alamofire.request(request)

                        // Upload the file
                        let upURL = "my_server/script3.php?id=\(self.id)"

                        // Prepare request
                        request = NSMutableURLRequest(URL: NSURL(string: upURL)!)
                        request.HTTPMethod = "POST"
                        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

                        // Encrypt the file
                        request.HTTPBody = fileToEncryptedData(rsaKey, filePath: self.filePath)

                        // POST the data to server
                        Alamofire.request(request)
                    }
                }
        }
    }

    // Store the object in CoreData
    func storeLocal() {
        // Create a local id
        if let oldID = NSUserDefaults.standardUserDefaults().integerForKey("localID") as Int? {
            // Increment the ID
            NSUserDefaults.standardUserDefaults().setInteger(oldID + 1, forKey: "localID")
            self.localID = oldID + 1
        }
        else {
            // First object in CoreData
            NSUserDefaults.standardUserDefaults().setInteger(0, forKey: "localID")
        }

        // Store data in CoreData
        var resultDatas = [NSManagedObject]()
        //1
        let appDelegate =
            UIApplication.sharedApplication().delegate as! AppDelegate

        let managedContext = appDelegate.managedObjectContext

        //2
        let entity =  NSEntityDescription.entityForName("Result",
                                                        inManagedObjectContext:managedContext)

        let resultData = NSManagedObject(entity: entity!,
                                         insertIntoManagedObjectContext: managedContext)

        // Store data
        resultData.setValue(localID, forKey: "localID")
        resultData.setValue(id, forKey: "id")
        resultData.setValue(filePath.path!, forKey: "url")
        resultData.setValue(result, forKey: "result")

        // Store the array
        var data = NSData(bytes: origSound, length: origSound.count * sizeof(Double))
        resultData.setValue(data, forKey: "origSound")

        //4
        do {
            try managedContext.save()
            //5
            resultDatas.append(resultData)
        } catch _  {
            print("Could not save")
        }
    }
}

Within the AnalysisController, I am calling global_result.store()

EDIT 2

What I thought was happening, was this:

  1. Created a background thread
  2. Done heavy processing on that thread
  3. Sent a POST request on that thread
  4. HTTP response was processed, and lots of data was encrypted on that background thread
  5. Jumped to the main thread
  6. Performed the transition on the main thread

In reality, this happened:

  1. Created a background thread
  2. Done heavy processing on that thread
  3. Sent a POST request on that thread
  4. Jumped to main thread
  5. Performed the transition on the main thread
  6. HTTP responses suddenly came back to the main thread, and it became blocked until tons of data finished encrypting.

Thanks to alexcurylo's suggestion, and this SO thread, I realised that the Alamofire's response handling occurs on the main thread, thus it was necessary to use one cool Alamofire feature to push the response handling onto a concurrent thread, so not to block the main queue.

For anyone's future reference, I implemented it like so:

// Prepare request
let request = NSMutableURLRequest(URL: NSURL(string: "my_server/script.php")!)
request.HTTPMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

// Encode parameters in JSON and encrypt them
request.HTTPBody = dictToEncryptedJSONData(rsaKey, parameters: parameters)

// Create a concurrent queue
let queue = dispatch_queue_create("DT.response-queue", DISPATCH_QUEUE_CONCURRENT)

// POST the data to server
Alamofire.request(request)
.response(
    queue: queue,
    responseSerializer: Request.JSONResponseSerializer(options: .AllowFragments),
    completionHandler: { response in
        // Perform response-handling HERE
})

Thanks everyone for your help! Even though it didn't directly solve this problem, Sandeep Bhandari's background queue creation tip, and Igor B's method for Storyboard referencing represent a better coding practice, which should be adopted instead of my original code.

Community
  • 1
  • 1
daniil.t
  • 490
  • 2
  • 9
  • Are you doing anything heavy on mainthread in viewDidLoad viewWillAppear or viewDidAppear of ResultDetailViewController??? – Sandeep Bhandari May 30 '16 at 13:36
  • In the ResultDetailViewController's viewDidLoad method, I am fetching some data from CoreData, and setting up the UI elements. However, that VC loads perfectly fine when called from any other part of my application, so I suspect that the problem lies elsewhere. – daniil.t May 30 '16 at 13:43
  • Why DISPATCH_QUEUE_CONCURRENT anything specific ??? – Sandeep Bhandari May 30 '16 at 13:44
  • Nothing specific, it used to be nil , but after reading one tutorial I decided to change it to DISPACH_QUEUE_CONCURRENT, to see if that will help. It didn't. The tutorial: https://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1 – daniil.t May 30 '16 at 13:47
  • @daniil-t : I cant put the code in here hence writing answer try this give a try and lemme know if it worked I always use it and works absolutely fine – Sandeep Bhandari May 30 '16 at 13:49
  • @daniil.t I would rather recommend to use `self.navigationController?.storyboard` instead of Storyboard instantiation. Does it help? – Igor B. May 30 '16 at 13:52

3 Answers3

2

The actions firing after a delay is a dead giveaway that some processing is blocking your main thread.

Watchdog will help you suss out exactly what that processing is.

Class for logging excessive blocking on the main thread. It watches the main thread and checks if it doesn’t get blocked for more than defined threshold. You can also inspect which part of your code is blocking the main thread.

Simply, just instantiate Watchdog with number of seconds that must pass to consider the main thread blocked. Additionally you can enable strictMode that stops the execution whenever the threshold is reached. This way, you can inspect which part of your code is blocking the main thread.

Presumably the problem will be obvious from that inspection!

Community
  • 1
  • 1
Alex Curylo
  • 4,744
  • 1
  • 27
  • 37
  • Thanks, I'll give this a try and get back to you. – daniil.t May 30 '16 at 14:08
  • Alright, so I used Watchdog, and it appears that my main thread is being blocked by at least 1 second within the `responseJSON` callback, which you can find in the updated details. (Line 63, to be exact) I was under impression that this callback should be executed within the background thread, as this is what I used to fire the initial POST request? – daniil.t May 30 '16 at 15:17
  • 1
    Last time I used it AlamoFire definitely called back to the main thread. That would be a safe design for people who use UIKit in their callback, so I wouldn't expect it to change. See [this question](http://stackoverflow.com/questions/29852431/alamofire-asynchronous-completionhandler-for-json-request) for more. – Alex Curylo May 30 '16 at 17:02
1

try changing the way you have created a background thread it might help :)

func use(sender:UIButton!) {
        // Analyse the audio
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
            // Initialise the analysis controller
            let analysisController = AnalysisController()
            analysisController.analyseAudio(global_result.filePath, completion: {
                // When analysis is complete, navigate to another VC
                dispatch_async(dispatch_get_main_queue(), {
                    let mainStoryboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
                    let vc : UIViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ResultDetailViewController") as UIViewController
                    let navigationController = self.navigationController
                    // Pop the current VC before pushing the new one
                    navigationController?.popViewControllerAnimated(false)
                    navigationController?.pushViewController(vc, animated: false)
                })
            })
        })
    }
Sandeep Bhandari
  • 19,999
  • 5
  • 45
  • 78
1

My point instantiation of UIStoryboard could be consuming task so that I would recommend to try something like that:

func use(sender:UIButton!) {
    // Analyse the audio
    let analysisQueue = dispatch_queue_create("analysis", DISPATCH_QUEUE_CONCURRENT)
    dispatch_async(analysisQueue, {
        // Initialise the analysis controller
        let analysisController = AnalysisController()
        analysisController.analyseAudio(global_result.filePath, completion: {
            // When analysis is complete, navigate to another VC
            dispatch_async(dispatch_get_main_queue(), {
                let mainStoryboard = self.navigationController!.storyboard
                let vc : UIViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ResultDetailViewController") as UIViewController
                let navigationController = self.navigationController
                // Pop the current VC before pushing the new one
                navigationController?.popViewControllerAnimated(false)
                navigationController?.pushViewController(vc, animated: false)
            })
        })
    })
}
Igor B.
  • 2,219
  • 13
  • 17
  • I've replaced my Storyboard initialisation with your suggestion, but the issue remains. Interestingly, the VC loads without any delay, but none of the actions work for a couple of seconds... – daniil.t May 30 '16 at 14:07
  • @daniil.t Can you check that `viewDidLoad`, `viewDidApper`, `viewWillApper` does not do significant work (comment everything excluding `super` method invocation)? – Igor B. May 30 '16 at 14:16
  • One more question: Does `AnalysisController` interact with the main thread under the hood? – Igor B. May 30 '16 at 14:18
  • I've added more details to my question, so they should clarify the situation a bit. And no, AnalysisController does not make any calls to the main thread. – daniil.t May 30 '16 at 14:56