Files
swift-nio/Sources/_NIODataStructures/PriorityQueue.swift
Si Beaumont ea78d5aadc Add EventLoop APIs for simpler scheduling of callbacks (#2759)
## Motivation

The current `scheduleTask` APIs make use of _both_ callbacks and
promises, which leads to confusing semantics. For example, on
cancellation, users are notified in two ways: once via the promise and
once via the callback. Additionally the way the API is structured
results in unavoidable allocations—for the closures and the
promise—which could be avoided if we structured the API differently.

## Modifications

This PR introduces new protocol requirements on `EventLoop`:

```swift
protocol EventLoop {
    // ...
    @discardableResult
    func scheduleCallback(at deadline: NIODeadline, handler: some NIOScheduledCallbackHandler) throws -> NIOScheduledCallback

    @discardableResult
    func scheduleCallback(in amount: TimeAmount, handler: some NIOScheduledCallbackHandler) throws -> NIOScheduledCallback

    func cancelScheduledCallback(_ scheduledCallback: NIOScheduledCallback)
}
```

Default implementations have been provided that call through to
`EventLoop.scheduleTask(in:_:)` to not break existing `EventLoop`
implementations, although this implementation will be (at least) as slow
as using `scheduleTask(in:_:)` directly.

The API is structured to allow for `EventLoop` implementations to
provide a custom implementation, as an optimization point and this PR
provides a custom implementation for `SelectableEventLoop`, so that
`MultiThreadedEventLoopGroup` can benefit from a faster implementation.

Finally, this PR adds benchmarks to measure the performance of setting a
simple timer using both `scheduleTask(in:_:)` and
`scheduleCallback(in:_:)` APIs using a `MultiThreadedEventLoopGroup`.

## Result

A simpler and more coherent API surface.

There is also a small performance benefit for heavy users of this API,
e.g. protocols that make extensive use of timers: when using MTELG to
repeatedly set a timer with the same handler, switching from
`scheduleTask(in:_:)` to `scheduleCallback(in:_:)` reduces almost all
allocations (and amortizes to zero allocations) and is ~twice as fast.

```
MTELG.scheduleCallback(in:_:)
╒═══════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│ Metric                │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞═══════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│ Malloc (total) *      │       0 │       0 │       0 │       0 │       0 │       0 │       0 │    1109 │
╘═══════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛

MTELG.scheduleTask(in:_:)
╒═══════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│ Metric                │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞═══════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│ Malloc (total) *      │       4 │       4 │       4 │       4 │       4 │       4 │       4 │     576 │
╘═══════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛
```
2024-10-03 20:14:28 +00:00

108 lines
2.5 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2021 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
//
//===----------------------------------------------------------------------===//
public struct PriorityQueue<Element: Comparable> {
@usableFromInline
internal var _heap: Heap<Element>
@inlinable
public init() {
self._heap = Heap()
}
@inlinable
public mutating func remove(_ key: Element) {
self._heap.remove(value: key)
}
@discardableResult
@inlinable
public mutating func removeFirst(where shouldBeRemoved: (Element) throws -> Bool) rethrows -> Element? {
try self._heap.removeFirst(where: shouldBeRemoved)
}
@inlinable
public mutating func push(_ key: Element) {
self._heap.append(key)
}
@inlinable
public func peek() -> Element? {
self._heap.storage.first
}
@inlinable
public var isEmpty: Bool {
self._heap.storage.isEmpty
}
@inlinable
@discardableResult
public mutating func pop() -> Element? {
self._heap.removeRoot()
}
@inlinable
public mutating func clear() {
self._heap = Heap()
}
}
extension PriorityQueue: Equatable {
@inlinable
public static func == (lhs: PriorityQueue, rhs: PriorityQueue) -> Bool {
lhs.count == rhs.count && lhs.elementsEqual(rhs)
}
}
extension PriorityQueue: Sequence {
public struct Iterator: IteratorProtocol {
@usableFromInline
var _queue: PriorityQueue<Element>
@inlinable
public init(queue: PriorityQueue<Element>) {
self._queue = queue
}
@inlinable
public mutating func next() -> Element? {
self._queue.pop()
}
}
@inlinable
public func makeIterator() -> Iterator {
Iterator(queue: self)
}
}
extension PriorityQueue {
@inlinable
public var count: Int {
self._heap.count
}
}
extension PriorityQueue: CustomStringConvertible {
@inlinable
public var description: String {
"PriorityQueue(count: \(self.count)): \(Array(self))"
}
}
extension PriorityQueue: Sendable where Element: Sendable {}
extension PriorityQueue.Iterator: Sendable where Element: Sendable {}