/*:#localized(key: "FanningOutWithTaskGroup")
 # Fanning out with `withTaskGroup`

 You open a group with `withTaskGroup(of:)`, telling it the type each child will
 return. Inside, you call `group.addTask { ... }` once per item (as many times as
 you like) and then read the results by iterating the group with `for await`.
 */
//#-editable-code
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let frameCount = 8

func processFrame(_ index: Int) async -> Double {
    try? await Task.sleep(for: .milliseconds(500))
    return Double(index) * 1.5
}

Task {
    let start = Date()

    let total = await withTaskGroup(of: Double.self) { group in
        // Spawn one child task per frame: a dynamic number of them.
        for index in 1...frameCount {
            group.addTask {
                await processFrame(index)
            }
        }

        // Collect results as each child finishes.
        var sum = 0.0
        for await brightness in group {
            sum += brightness
        }
        return sum
    }

    let elapsed = Date().timeIntervalSince(start)
    print("Parallel: combined \(frameCount) frames = \(total)")
    print(String(format: "Took about %.2f s", elapsed))
}
//#-end-editable-code
/*:#localized(key: "FanningOutWithTaskGroup-2")
 Read the shape carefully, because it's the whole chapter:

 * `withTaskGroup(of: Double.self)` opens the group. `Double` is what each child
 returns: here, one frame's brightness.
 * `group.addTask { ... }` schedules a **child task**. Each runs concurrently with
 its siblings, and each returns a `Double`.
 * `for await brightness in group` pulls results out **one at a time as they
 complete**. The group is an `AsyncSequence` of its children's return values.
 * The value you `return` from the closure becomes the result of the whole
 `await withTaskGroup(...)` expression.

 To feel the payoff, compare it with running the same work sequentially, each frame
 awaited before the next one starts:
 */
//#-editable-code
Task {
    let start = Date()

    var total = 0.0
    for index in 1...frameCount {
        total += await processFrame(index)   // wait for each before the next
    }

    let elapsed = Date().timeIntervalSince(start)
    print("Sequential: combined \(frameCount) frames = \(total)")
    print(String(format: "Took about %.2f s", elapsed))

    PlaygroundPage.current.finishExecution()
}
//#-end-editable-code
/*:#localized(key: "FanningOutWithTaskGroup-3")
 The sequential version awaits each frame before starting the next, so 8 frames at
 half a second each take about **4 seconds**. The task-group version above starts
 all 8 at once, so they overlap and the whole batch finishes in about **half a
 second**.

 * callout(Try it): Bump `frameCount` to `40` in the code above and rerun; the
 parallel time barely moves. A plain sequential loop over 40 frames, by contrast,
 would climb toward 20 seconds.

 Notice *why* this overlaps even if your device has only a couple of cores: each
 frame here is mostly **waiting** (that `Task.sleep` stands in for reading a file),
 and, just as you saw in the chapter [Why Concurrency](Why%20Concurrency.playgroundchapter/Overview), waiting tasks step aside for each other. Real
 calibration is CPU work too, and there the group also lets the system spread
 frames across whatever cores exist. Either way, the group turns a pile of
 independent jobs into one concurrent batch.

 [Previous](@previous)  |  page 3 of 9  |  [Next: Results Arrive in Completion Order](@next)
 */
