I tried to refactor a Firebase operation from the old completion handler to the new Task.init()
and it seems that the operation is now taking longer. Am I doing something wrong? Are the await calls not being done concurrently (which is the reason I am calling both operations at the same time and counting how many finished with the completion handler approach)? Any suggestions for what might be causing the slower execution time?
Thank you in advance.
Completion handler approach (0.73s)
Previously I had this method:
extension Firebase { static func getAll<T: Decodable>(_ subCollection: SubCollection, completion: @escaping (_ result: [T]?, _ error: Error?) -> Void) { db.collection("userData").document(currentUser!.uid).collection("\(subCollection)").getDocuments(completion: { (querySnapshot, error) in var documents: [T] = [] if let error { print(error.localizedDescription) } if let querySnapshot { for document in querySnapshot.documents { if let decodedDocument = try? document.data(as: T.self) { documents.append(decodedDocument) } else { print("Failed to decode a retrieved document of type \(T.self) at getAll") } } } completion(documents, error) AppNotification.post(.firebseOperationCompleted) }) } }
Which I would use like this:
class Loading: UIViewController { var error: Error? var operationsCompleted = 0 let start = CFAbsoluteTimeGetCurrent() private func fetchData() { operationsCompleted = 0 Firebase.getAll(.goals, completion: { (results: [Goal]?, error) in if let results { UltraDataStorage.goals = results } if let error { self.error = error } self.operationsCompleted = 1 }) Firebase.getAll(.ideas, completion: { (results: [Idea]?, error) in if let results { UltraDataStorage.ideas = results } if let error { self.error = error } self.operationsCompleted = 1 }) @objc func advanceWhenOperationsComplete(){ print("Took \(CFAbsoluteTimeGetCurrent() - self.start) seconds") if operationsCompleted == 2 { // Proceed if error is nil } } override func viewDidLoad() { fetchData() AppNotification.observe(handler: self, name: .firebseOperationCompleted, function: #selector(advanceWhenOperationsComplete)) } }
Task.init()
approach (1.14s)
Now, I updated the
getAll
function:extension Firebase { static func getAll<T: Decodable>(_ subCollection: SubCollection) async -> Result<[T], Error> { do { let documents = try await db.collection("userData").document(currentUser!.uid).collection("\(subCollection)").getDocuments() var decodedDocuments: [T] = [] for document in documents.documents { if let decodedDocument = try? document.data(as: T.self) { decodedDocuments.append(decodedDocument) } else { print("Failed to decode a retrieved document of type \(T.self) at getAll") } } return.success(decodedDocuments) } catch { return.failure(error) } } }
And I am now calling it like this
class Loading: UIViewController { var error: Error? let start = CFAbsoluteTimeGetCurrent() private func fetchData() { Task.init(operation: { let goalsResult: Result<[Goal], Error> = await Firebase.getAll(.goals) switch goalsResult { case .success(let goals): UltraDataStorage.goals = goals case .failure(let error): self.error = error } let ideasResult: Result<[Idea], Error> = await Firebase.getAll(.ideas) switch ideasResult { case .success(let ideas): UltraDataStorage.ideas = ideas case .failure(let error): self.error = error } DispatchQueue.main.async { self.advanceWhenOperationsComplete() } }) } func advanceWhenOperationsComplete(){ print("Took \(CFAbsoluteTimeGetCurrent() - self.start) seconds") // Proceed when the async operations are completed } override func viewDidLoad() { fetchData() } }
CodePudding user response:
The performance difference is likely a result that the completion handler pattern is running the requests concurrently, but the async
-await
rendition is performing them sequentially. The latter is awaiting the result of the first asynchronous request before even initiating the next asynchronous request.
To get them to run concurrently, you can either use the async let
pattern (see SE-0317) or use a task group:
extension Firebase {
static func getAll<T: Decodable>(_ subCollection: SubCollection) async throws -> [T] {
try await db
.collection("userData")
.document(currentUser!.uid)
.collection("\(subCollection)")
.getDocuments()
.documents
.map { try $0.data(as: T.self) }
}
}
// you could use `async let`
private func fetchData1() async throws {
async let goals: [Goal] = Firebase.getAll(.goals)
async let ideas: [Idea] = Firebase.getAll(.ideas)
UltraDataStorage.goals = try await goals
UltraDataStorage.ideas = try await ideas
advanceWhenOperationsComplete()
}
// or task group
private func fetchData2() async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { UltraDataStorage.goals = try await Firebase.getAll(.goals) }
group.addTask { UltraDataStorage.ideas = try await Firebase.getAll(.ideas) }
try await group.waitForAll()
}
advanceWhenOperationsComplete()
}
(These might not be 100% right, as I do not implementations for all of these types and methods and therefore cannot compile this, but hopefully it illustrates the idea. Notably, I am nervous about the thread-safety of UltraDataStorage
, especially in the task group example. But, that is beyond the scope of this question.)
Bottom line, async let
is an intuitive way to run tasks concurrently and is most useful when dealing with a fixed, limited number of asynchronous tasks. Task groups shine when dealing with a variable number of tasks. That’s not the case here, but I include it for the sake of completeness.
Note, I’ve taken the liberty of excising the Result<[T], Error>
type, and instead throw
the error.