mirror of
https://github.com/apple/swift-nio.git
synced 2026-05-20 20:30:36 +00:00
**TL;DR** This change leads to a ~90% reduction in observed system CPU time for some use cases by waking a single thread, instead of all idle threads. # Changes Inlining the commit messages here. ## Add NIOThreadPool submit throughput benchmarks ### Motivation `NIOThreadPool` had no benchmarks measuring submit overhead. This makes it difficult to evaluate the cost of signalling changes or to catch latency regressions. ### Modifications Add thread pool submit benchmarks, covering use cases with 4-thread and 16-thread pools. ### Result `NIOThreadPool` submit throughput and context-switch overhead are now tracked by benchmarks. ### Benchmark Results <details> ``` NIOThreadPool.serial_wakeup(16 threads) ╒══════════════════════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╕ │ Metric │ p0 │ p25 │ p50 │ p75 │ p90 │ p99 │ p100 │ Samples │ ╞══════════════════════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╡ │ Context switches (K) │ 77 │ 78 │ 79 │ 80 │ 80 │ 92 │ 92 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Syscalls (total) (K) * │ 106 │ 107 │ 108 │ 109 │ 110 │ 116 │ 116 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (system CPU) (ms) * │ 1649 │ 1752 │ 1768 │ 1795 │ 1826 │ 1929 │ 1929 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (total CPU) (ms) * │ 1701 │ 1805 │ 1821 │ 1849 │ 1879 │ 1987 │ 1987 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (user CPU) (ms) * │ 51 │ 52 │ 52 │ 53 │ 53 │ 57 │ 57 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (wall clock) (ms) * │ 167 │ 177 │ 178 │ 181 │ 183 │ 200 │ 200 │ 30 │ ╘══════════════════════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╛ NIOThreadPool.serial_wakeup(4 threads) ╒══════════════════════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╕ │ Metric │ p0 │ p25 │ p50 │ p75 │ p90 │ p99 │ p100 │ Samples │ ╞══════════════════════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╡ │ Context switches (K) │ 44 │ 44 │ 44 │ 45 │ 45 │ 45 │ 45 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Syscalls (total) (K) * │ 65 │ 65 │ 65 │ 66 │ 66 │ 67 │ 67 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (system CPU) (ms) * │ 159 │ 162 │ 163 │ 165 │ 166 │ 169 │ 169 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (total CPU) (ms) * │ 178 │ 182 │ 183 │ 185 │ 186 │ 190 │ 190 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (user CPU) (ms) * │ 19 │ 19 │ 20 │ 20 │ 20 │ 21 │ 21 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (wall clock) (ms) * │ 76 │ 79 │ 79 │ 80 │ 80 │ 82 │ 82 │ 30 │ ╘══════════════════════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╛ ``` </details> ## Replace `ConditionLock` with wake-one signalling `NIOThreadPoolWorkAvailable` ### Motivation `NIOThreadPool` used `ConditionLock` which calls `pthread_cond_broadcast` on every state change, waking all threads when only one work item is enqueued. This causes a thundering-herd problem. ### Modifications Add `NIOThreadPoolWorkAvailable` in NIOConcurrencyHelpers that uses `pthread_cond_signal` (wake-one) for work submission and `pthread_cond_broadcast` only for shutdown. Replace `ConditionLock<_WorkState>` and the `_WorkState` enum in `NIOThreadPool` with this new primitive. ### Result Submitting a work item wakes exactly **one** thread instead of all threads. ### Benchmark Results <details> ``` NIOThreadPool.serial_wakeup(16 threads) ╒══════════════════════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╕ │ Metric │ p0 │ p25 │ p50 │ p75 │ p90 │ p99 │ p100 │ Samples │ ╞══════════════════════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╡ │ Context switches (K) │ 20 │ 20 │ 20 │ 20 │ 20 │ 20 │ 20 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Syscalls (total) (K) * │ 40 │ 40 │ 40 │ 40 │ 40 │ 40 │ 40 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (system CPU) (ms) * │ 47 │ 49 │ 49 │ 50 │ 50 │ 55 │ 55 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (total CPU) (ms) * │ 57 │ 58 │ 59 │ 60 │ 61 │ 67 │ 67 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (user CPU) (ms) * │ 10 │ 10 │ 10 │ 10 │ 10 │ 12 │ 12 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (wall clock) (ms) * │ 54 │ 55 │ 56 │ 56 │ 57 │ 65 │ 65 │ 30 │ ╘══════════════════════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╛ NIOThreadPool.serial_wakeup(4 threads) ╒══════════════════════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╤═══════════╕ │ Metric │ p0 │ p25 │ p50 │ p75 │ p90 │ p99 │ p100 │ Samples │ ╞══════════════════════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╪═══════════╡ │ Context switches (K) │ 20 │ 20 │ 20 │ 20 │ 20 │ 20 │ 20 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Syscalls (total) (K) * │ 40 │ 40 │ 40 │ 40 │ 40 │ 40 │ 40 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (system CPU) (ms) * │ 45 │ 46 │ 46 │ 47 │ 57 │ 75 │ 75 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (total CPU) (ms) * │ 54 │ 55 │ 56 │ 57 │ 68 │ 87 │ 87 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (user CPU) (μs) * │ 9055 │ 9372 │ 9478 │ 9765 │ 10887 │ 12585 │ 12585 │ 30 │ ├──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ Time (wall clock) (ms) * │ 52 │ 53 │ 53 │ 54 │ 64 │ 124 │ 124 │ 30 │ ╘══════════════════════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╧═══════════╛ ``` </details>
52 lines
1.5 KiB
Swift
52 lines
1.5 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2026 Apple Inc. and the SwiftNIO project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import Dispatch
|
|
import NIOCore
|
|
import NIOPosix
|
|
|
|
/// Submit one item at a time, waiting for completion before the next.
|
|
/// Every submit hits N sleeping threads — isolates wake-one vs wake-all.
|
|
final class NIOThreadPoolSerialWakeupBenchmark: Benchmark {
|
|
private let numberOfThreads: Int
|
|
private let numberOfTasks: Int
|
|
private var pool: NIOThreadPool!
|
|
|
|
init(numberOfThreads: Int, numberOfTasks: Int) {
|
|
self.numberOfThreads = numberOfThreads
|
|
self.numberOfTasks = numberOfTasks
|
|
}
|
|
|
|
func setUp() throws {
|
|
self.pool = NIOThreadPool(numberOfThreads: self.numberOfThreads)
|
|
self.pool.start()
|
|
}
|
|
|
|
func tearDown() {
|
|
try! self.pool.syncShutdownGracefully()
|
|
}
|
|
|
|
func run() throws -> Int {
|
|
let sem = DispatchSemaphore(value: 0)
|
|
for _ in 0..<self.numberOfTasks {
|
|
self.pool.submit { state in
|
|
precondition(state == .active)
|
|
sem.signal()
|
|
}
|
|
sem.wait()
|
|
}
|
|
return self.numberOfTasks
|
|
}
|
|
}
|