Skip to content

fix(up): stop pinning a CPU core during foreground wait#82

Open
Cyb3rDudu wants to merge 2 commits intoMcrich23:mainfrom
Cyb3rDudu:fix/up-foreground-cpu
Open

fix(up): stop pinning a CPU core during foreground wait#82
Cyb3rDudu wants to merge 2 commits intoMcrich23:mainfrom
Cyb3rDudu:fix/up-foreground-cpu

Conversation

@Cyb3rDudu
Copy link
Copy Markdown
Contributor

container-compose up (foreground) holds 100% of one core after all containers have started. Reproducible with any compose file run without -d:

services:
  idle:
    image: alpine:latest
    command: ["sleep", "60"]
$ container-compose up &
$ ps -o pcpu,comm -p $!
%CPU COMM
100.0 container-compose

The hot loop is ComposeUp.swift::waitForever:

for await _ in AsyncStream<Void>(unfolding: {}) {
    // This will never run
}

AsyncStream<Void>(unfolding: () async -> Void?) ends only when the closure returns nil. The empty closure returns (), which Swift auto-wraps as .some(()) — never nil — so the stream emits an infinite sequence of Void values with no await between them. The for-loop iterates as fast as the runtime can deliver them. The // This will never run comment is inverted: the body runs constantly; only fatalError("unreachable") is unreachable.

Fix is withUnsafeContinuation { _ in }: the task suspends until the process is killed, with zero CPU. After this change the same repro shows 0.0 container-compose.

Used withUnsafeContinuation rather than withCheckedContinuation because the latter logs a "continuation leaked" diagnostic at runtime — leaking is the intent here, since the contract is to wait until the process is killed.

Fixes #27.

`waitForever()` used `for await _ in AsyncStream<Void>(unfolding: {})`,
but `AsyncStream<Void>(unfolding: () async -> Void?)` ends only when the
closure returns `nil`. An empty closure returns `()`, which Swift
auto-wraps as `.some(())` — never `nil` — so the stream emits an
infinite sequence of `Void` values with no `await` between them. The
for-loop spins as fast as the runtime can iterate, holding 100% of one
core until the user kills the process.

Replace with `withUnsafeContinuation { _ in }` — the task suspends
indefinitely with zero CPU.

Fixes Mcrich23#27.
@Cyb3rDudu Cyb3rDudu mentioned this pull request May 4, 2026
Spawns a detached Task running `waitForever()`, sleeps 200 ms wall-clock,
and compares user-CPU consumed via `getrusage(RUSAGE_SELF)` before/after.
On the bug, the child task pins one core and the test sees ~200,000 µs of
user CPU (one core fully used). With the fix, consumption is negligible.
Threshold of 50,000 µs gives ~4x headroom for noisy CI baselines.

Verified the test fails on the previous behavior: 209,086 µs consumed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

High CPU usage

1 participant