Swift 5.5 Concurrency

Introduction

Concurrency features are now available in Swift 5.5, released as part of Apple Xcode 13 beta 1. Let’s update our Web Scraping code to use the Swift 5.5 concurrency APIs.

The updated code

extension Array {
  // TODO: When withoutActuallyEscaping() supports async closures, use withoutActuallyEscaping
  // on the group.spawnUnlessCancelled() closure, and then to remove the unnecessary @escaping decoration.
  func asyncMap<B>(_ transform: @escaping (Element) async throws -> B) async throws -> [B] {
    try await withThrowingTaskGroup(of: (Int, B).self) { group in
      for i in self.indices {
        _ = group.asyncUnlessCancelled {
          (i, try await transform(self[i]))
        }
      }
      var result = [B?](repeating: nil, count: count)
      while let (index, transformed) = try await group.next() {
        result[index] = transformed
      }
      return result.map { $0! }
    }
  }
}

func fetch(url: URL) async throws -> String {
  try await String(decoding: URLSession.shared.data(from:url).0, as: UTF8.self)
}

func scrapeHouseplants(url: URL) async throws -> HouseplantCategoryDictionary {
  return try reduceHouseplantInfos(
    infos: await scrapeListOfHouseplants(url: url, html: fetch(url: url))
      .asyncMap { ticket in
        (ticket.key, try scrapeHouseplantInfo(url: ticket.url,
                                              html:await fetch(url: ticket.url)))
      }
  )
}

func scrapeAsync() async {
  let url = URL(string:"https://en.wikipedia.org/wiki/Houseplant")!
  _ = try! await scrapeHouseplants(url:url)
}

Unfortunately there’s no current software release from Apple that allows Swift Async code to work with a MacOS 11.4 Big Sur command line. So I can’t compare the performance of this new code against the previous version.

A release version of this code runs on an iPhone 12 Pro Max, and on that device it takes under two seconds to run.

Instruments shows that all the fetches are requested in parallel. It’s not clear to me why the results come back in groups. It may be there is some throttlng going on in iOS or in Wikipedia.

Instruments Networking trace showing 57 fetches being done in parallel.