Skip to content

Commit 1b18131

Browse files
committed
Remove AsyncIterator: Sendable requirement from debounce
# Motivation The current implementation of `AsyncDebounceSequence` requires the base `AsyncIterator` to be `Sendable`. This is causing two problems: 1. It only allows users to use `debounce` if their `AsyncSequence.AsyncIterator` is `Sendable` 2. In `debounce` we are creating a lot of new `Task`s and reating `Task`s is not cheap. My main goal of this PR was to remove the `Sendable` constraint from `debounce`. # Modification This PR overhauls the implementation of `debounce` and aligns it with the implementation of the open `merge` PR apple#185 . The most important changes are this: - I removed the `Sendable` requirement from the base sequences `AsyncIterator`. - Instead of creating new Tasks for the sleep and for the upstream consumption. I am now creating one Task and manipulate it by signalling continuations - I am not cancelling the sleep. Instead I am recalculating the time left to sleep when a sleep finishes. # Result In the end, this PR swaps the implementation of `AsyncDebounceSequence` and drops the `Sendable` constraint and passes all tests. Furthermore, on my local performance testing I saw up 150% speed increase in throughput.
1 parent 68c8dc2 commit 1b18131

File tree

6 files changed

+1172
-121
lines changed

6 files changed

+1172
-121
lines changed

Sources/AsyncAlgorithms/AsyncDebounceSequence.swift

Lines changed: 0 additions & 121 deletions
This file was deleted.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Async Algorithms open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
extension AsyncSequence {
13+
/// Creates an asynchronous sequence that emits the latest element after a given quiescence period
14+
/// has elapsed by using a specified Clock.
15+
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
16+
public func debounce<C: Clock>(for interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) -> AsyncDebounceSequence<Self, C> where Self: Sendable {
17+
AsyncDebounceSequence(self, interval: interval, tolerance: tolerance, clock: clock)
18+
}
19+
20+
/// Creates an asynchronous sequence that emits the latest element after a given quiescence period
21+
/// has elapsed.
22+
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
23+
public func debounce(for interval: Duration, tolerance: Duration? = nil) -> AsyncDebounceSequence<Self, ContinuousClock> where Self: Sendable {
24+
self.debounce(for: interval, tolerance: tolerance, clock: .continuous)
25+
}
26+
}
27+
28+
/// An `AsyncSequence` that emits the latest element after a given quiescence period
29+
/// has elapsed.
30+
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
31+
public struct AsyncDebounceSequence<Base: AsyncSequence, C: Clock>: Sendable where Base: Sendable {
32+
/// This class is needed to hook the deinit to observe once all references to the ``AsyncDebounceSequence`` are dropped.
33+
///
34+
/// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncDebounceSequence`` struct itself.
35+
final class InternalClass: Sendable {
36+
fileprivate let storage: DebounceStorage<Base, C>
37+
38+
fileprivate init(storage: DebounceStorage<Base, C>) {
39+
self.storage = storage
40+
}
41+
42+
deinit {
43+
storage.sequenceDeinitialized()
44+
}
45+
}
46+
47+
/// The internal class to hook the `deinit`.
48+
let internalClass: InternalClass
49+
50+
/// The underlying storage
51+
fileprivate var storage: DebounceStorage<Base, C> {
52+
self.internalClass.storage
53+
}
54+
55+
/// Initializes a new ``AsyncDebounceSequence``.
56+
///
57+
/// - Parameters:
58+
/// - base: The base sequence.
59+
/// - interval: The interval to debounce.
60+
/// - tolerance: The tolerance of the clock.
61+
/// - clock: The clock.
62+
public init(_ base: Base, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) {
63+
let storage = DebounceStorage<Base, C>(
64+
base: base,
65+
interval: interval,
66+
tolerance: tolerance,
67+
clock: clock
68+
)
69+
self.internalClass = .init(storage: storage)
70+
}
71+
}
72+
73+
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
74+
extension AsyncDebounceSequence: AsyncSequence {
75+
public typealias Element = Base.Element
76+
77+
public func makeAsyncIterator() -> AsyncIterator {
78+
AsyncIterator(storage: self.internalClass.storage)
79+
}
80+
}
81+
82+
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
83+
extension AsyncDebounceSequence {
84+
public struct AsyncIterator: AsyncIteratorProtocol {
85+
/// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped.
86+
///
87+
/// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself.
88+
final class InternalClass: Sendable {
89+
private let storage: DebounceStorage<Base, C>
90+
91+
fileprivate init(storage: DebounceStorage<Base, C>) {
92+
self.storage = storage
93+
self.storage.iteratorInitialized()
94+
}
95+
96+
deinit {
97+
self.storage.iteratorDeinitialized()
98+
}
99+
100+
func next() async rethrows -> Element? {
101+
try await self.storage.next()
102+
}
103+
}
104+
105+
let internalClass: InternalClass
106+
107+
fileprivate init(storage: DebounceStorage<Base, C>) {
108+
self.internalClass = InternalClass(storage: storage)
109+
}
110+
111+
public mutating func next() async rethrows -> Element? {
112+
try await self.internalClass.next()
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)