/*:#localized(key: "CappingConcurrency")
 # Capping how many run at once

 The basic pattern adds *every* task up front. For 8 frames that's fine. For 500,
 you've just asked the system to start 500 children at once, each one perhaps
 holding a decoded frame in memory, or hammering the same disk or server. That can
 blow up memory, thrash the disk, or get you rate-limited. You usually want a
 **concurrency limit**: keep at most `N` frames in flight, and start the next only
 as one finishes.

 In the classic model you had a native knob for this: `OperationQueue`'s
 `maxConcurrentOperationCount` from chapter [Operations](Operations.playgroundchapter/Overview), or a `DispatchSemaphore` from
 the chapter [Groups and Semaphores](Groups%20and%20Semaphores.playgroundchapter/Overview). **Task groups have no such built-in setting**: there's no
 `group.maxConcurrency = 4`. Instead you express the cap by controlling *when you
 call `addTask`*, and the standard idiom is this: **prime** the group with `N`
 tasks, then inside the collection loop, add one new task each time a result
 comes out. That keeps the in-flight count hovering at `N`.
 */
//#-editable-code
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

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

Task {
    let frameCount = 20
    let maxInFlight = 4
    var combined = 0.0

    await withTaskGroup(of: Double.self) { group in
        var nextIndex = 1

        // Prime the pump: start up to maxInFlight tasks.
        while nextIndex <= min(maxInFlight, frameCount) {
            let index = nextIndex
            group.addTask { await processFrame(index) }
            nextIndex += 1
        }

        // Each time one finishes, top up with the next, never more
        // than maxInFlight running at once.
        for await brightness in group {
            combined += brightness
            if nextIndex <= frameCount {
                let index = nextIndex
                group.addTask { await processFrame(index) }
                nextIndex += 1
            }
        }
    }

    print("Combined \(frameCount) frames, \(maxInFlight) at a time: \(combined)")
    PlaygroundPage.current.finishExecution()
}
//#-end-editable-code
/*:#localized(key: "CappingConcurrency-2")
 **Watch it run:** all 20 frames still get processed, and the result is the same
 as the fan-out-everything version. But at no instant are more than 4 running.
 With 20 frames in batches of 4 at half a second each, the whole thing takes about
 **2.5 seconds**, slower than firing all 20 at once, but with a bounded,
 predictable footprint. That trade, a little wall-clock time for a hard cap on
 memory and load, is exactly why you'd reach for it. Tune `maxInFlight` to the
 resource you're protecting.

 * callout(Try it): Set `maxInFlight` to `1`; you've turned the group right back
 into the sequential, one-at-a-time version.

 [Previous](@previous)  |  page 8 of 9  |  [Next: Wrapping Up](@next)
 */
