Skip to content

Bring WaitGroup completion to parity with waitgroup #397

Open

Description

@tisonkun

Completing a group with one pending observer took nearly three times as long as waitgroup 0.1.2 in the original comparison. Both adapters create a group, register the observer, drop the worker handles and poll to completion. The four-worker case drops four handles sequentially on the same thread.

Apple M4 Max, macOS arm64, rustc 1.101.0-nightly. Each cell gives rank, elapsed time relative to Asyncband and the whole-batch median over 10 interleaved runs. Lower is faster. Ecosystem measurements

Scenario Fixed rounds Asyncband waitgroup
One worker, one observer 3,500,000 #2, 1.00× (284 ms) #1, 0.35× (98 ms)
Four workers, one observer 2,600,000 #2, 1.00× (305 ms) #1, 0.44× (134 ms)

The baseline maintains a participant count as well as Arc ownership. Even the first observer uses a locked, cancellable waker collection. A candidate shares allocation ownership across participants, stores the first waker inline and uses the collection for additional observers. In two launches the one-worker medians were 81 vs 97 ms and 86 vs 97 ms (Asyncband vs waitgroup). With four workers they were 119 vs 125 ms and 127 vs 124 ms. That ranking changed between launches. Candidate and raw samples in PR #395

First contribution

Run the command on 36ba6f2 and on the candidate in #395 on the same machine. Report the two completion rows on independent launches, ideally also on Linux. The candidate adds a four-observer drain case. Check its before/after result separately because its peers compose a different multi-observer mechanism.

cargo x bench --bench ecosystem -- waitgroup/ --repeat 10 --raw

The goal is repeatable parity or better on both original rows while preserving completion visibility, independent observers, cancellation and wakeups. Measurements and correctness review on #395 are both welcome.

Performance tracking issue #396

Activity

  1. rokadepiyush49-rgb commented on Nov 6, 2026

    @rokadepiyush49-rgb

    Could you assign this to me? I'd run waitgroup/ on 36ba6f2 and the #395 head on an Apple M4 (10 cores, macOS 27.0.1), with independent launches, then repeat on Linux. I'd report the two completion rows and the four-observer drain separately.

    From #395's table, complete-4-workers is the row that's still behind (135 vs 127 ms), so I'd start there.

    One question: waiters.rs uses RwLock only for writes. In a small standalone probe on this Mac, a fresh Mutex allocated 64 bytes on first lock and a fresh RwLock didn't. Was that the reason for choosing it?

    I haven't run the benchmarks yet. I used Claude to help me read the code and write the probe.

  2. tisonkun commented on Nov 6, 2026

    @tisonkun
    MemberAuthor

    Thanks for your interest! We don't assign issues to individual contributors. Anyone is welcome to submit a PR directly for review.

  3. nanami-0713 commented on Nov 9, 2026

    @nanami-0713
    Contributor

    Reproduced both scenarios on Apple M5 (10 cores), macOS 27.0.1, rustc 1.99.1 stable, in a disposable checkout of 629e073 (main), with cargo x bench --bench ecosystem -- waitgroup --repeat 10 --raw. Cells give the median of ten repetitions, with min..max ranges.

    Scenario Rounds Asyncband waitgroup
    complete-waiter 3,500,000 175 ms (172..450) 61 ms (61..94)
    complete-4-workers 2,600,000 170 ms (168..173) 66 ms (65..67)

    A #[global_allocator] counting probe over 1,000,000 complete-waiter rounds records three allocations per round for Asyncband (208 bytes in total) against one (40 bytes) for waitgroup 0.1.2. Backtraces identify all three sites, and each recurs on every round:

    • 80 B: the Arc<State> allocation in WaitGroup::new
    • 64 B: pthread-mutex boxing when the waiters lock is first taken in Wait::poll (macOS only; futex-based targets lock inline)
    • 64 B: the first Arena<Waker> insert for the registered waker, also under Wait::poll

    I then picked up #395 (head 540858b) and re-ran the same protocol back-to-back in the same session (the waitgroup columns below come from that session's run). Both registered scenarios close the gap, and the fan-in and drain scenarios the branch adds sit at parity or better:

    Scenario Rounds Asyncband @ 629e073 Asyncband @ #395 waitgroup
    complete-waiter 3,500,000 175 ms 52 ms (52..95) 61 ms (61..61)
    complete-4-workers 2,600,000 170 ms 68 ms (66..70) 64 ms (64..83)
    wait-after-work 5,500,000 — 61 ms (60..66) 64 ms (64..65)
    nested-4-workers 3,000,000 — 85 ms (85..87) 86 ms (86..86)
    tasks-4-workers-4w 25,000 — 168 ms (166..219) 168 ms (167..188)
    drain-4-observers 2,500,000 — 129 ms (128..130) 502 ms (492..506), shared

    On the #395 tree the same probe records one allocation per round — 56 B, 16 B above waitgroup's 40 B, covering the inline first-waker slot. In drain-4-observers the comparison set also includes a Tokio TaskTracker adapter at 352 ms (351..370), 2.73x Asyncband's time.

    PR #421 continues #395 on top of current main: the series rebases cleanly — git range-diff shows three commits identical and only the CHANGELOG entry moving to the current Unreleased section — and cargo x test and cargo x lint pass on the rebased branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions