Skip to content

Commit 99c9965

Browse files
Support Clock-based sleep APIs (#216)
1 parent 60ca6b5 commit 99c9965

File tree

3 files changed

+98
-32
lines changed

3 files changed

+98
-32
lines changed

IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ import Darwin
88

99
#if compiler(>=5.5)
1010

11+
func performanceNow() -> Double {
12+
return JSObject.global.performance.now.function!().number!
13+
}
14+
15+
func measure(_ block: () async throws -> Void) async rethrows -> Double {
16+
let start = performanceNow()
17+
try await block()
18+
return performanceNow() - start
19+
}
20+
1121
func entrypoint() async throws {
1222
struct E: Error, Equatable {
1323
let value: Int
@@ -61,10 +71,10 @@ func entrypoint() async throws {
6171
}
6272

6373
try await asyncTest("Task.sleep(_:)") {
64-
let start = time(nil)
65-
try await Task.sleep(nanoseconds: 2_000_000_000)
66-
let diff = difftime(time(nil), start);
67-
try expectGTE(diff, 2)
74+
let diff = try await measure {
75+
try await Task.sleep(nanoseconds: 200_000_000)
76+
}
77+
try expectGTE(diff, 200)
6878
}
6979

7080
try await asyncTest("Job reordering based on priority") {
@@ -102,19 +112,19 @@ func entrypoint() async throws {
102112

103113
try await asyncTest("Async JSClosure") {
104114
let delayClosure = JSClosure.async { _ -> JSValue in
105-
try await Task.sleep(nanoseconds: 2_000_000_000)
115+
try await Task.sleep(nanoseconds: 200_000_000)
106116
return JSValue.number(3)
107117
}
108118
let delayObject = JSObject.global.Object.function!.new()
109119
delayObject.closure = delayClosure.jsValue
110120

111-
let start = time(nil)
112-
let promise = JSPromise(from: delayObject.closure!())
113-
try expectNotNil(promise)
114-
let result = try await promise!.value
115-
let diff = difftime(time(nil), start)
116-
try expectGTE(diff, 2)
117-
try expectEqual(result, .number(3))
121+
let diff = try await measure {
122+
let promise = JSPromise(from: delayObject.closure!())
123+
try expectNotNil(promise)
124+
let result = try await promise!.value
125+
try expectEqual(result, .number(3))
126+
}
127+
try expectGTE(diff, 200)
118128
}
119129

120130
try await asyncTest("Async JSPromise: then") {
@@ -124,18 +134,18 @@ func entrypoint() async throws {
124134
resolve(.success(JSValue.number(3)))
125135
return .undefined
126136
}.jsValue,
127-
1_000
137+
100
128138
)
129139
}
130140
let promise2 = promise.then { result in
131-
try await Task.sleep(nanoseconds: 1_000_000_000)
141+
try await Task.sleep(nanoseconds: 100_000_000)
132142
return String(result.number!)
133143
}
134-
let start = time(nil)
135-
let result = try await promise2.value
136-
let diff = difftime(time(nil), start)
137-
try expectGTE(diff, 2)
138-
try expectEqual(result, .string("3.0"))
144+
let diff = try await measure {
145+
let result = try await promise2.value
146+
try expectEqual(result, .string("3.0"))
147+
}
148+
try expectGTE(diff, 200)
139149
}
140150

141151
try await asyncTest("Async JSPromise: then(success:failure:)") {
@@ -145,7 +155,7 @@ func entrypoint() async throws {
145155
resolve(.failure(JSError(message: "test").jsValue))
146156
return .undefined
147157
}.jsValue,
148-
1_000
158+
100
149159
)
150160
}
151161
let promise2 = promise.then { _ in
@@ -164,26 +174,43 @@ func entrypoint() async throws {
164174
resolve(.failure(JSError(message: "test").jsValue))
165175
return .undefined
166176
}.jsValue,
167-
1_000
177+
100
168178
)
169179
}
170180
let promise2 = promise.catch { err in
171-
try await Task.sleep(nanoseconds: 1_000_000_000)
181+
try await Task.sleep(nanoseconds: 100_000_000)
172182
return err
173183
}
174-
let start = time(nil)
175-
let result = try await promise2.value
176-
let diff = difftime(time(nil), start)
177-
try expectGTE(diff, 2)
178-
try expectEqual(result.object?.message, .string("test"))
184+
let diff = try await measure {
185+
let result = try await promise2.value
186+
try expectEqual(result.object?.message, .string("test"))
187+
}
188+
try expectGTE(diff, 200)
179189
}
180190

181-
// FIXME(katei): Somehow it doesn't work due to a mysterious unreachable inst
182-
// at the end of thunk.
183-
// This issue is not only on JS host environment, but also on standalone coop executor.
184191
try await asyncTest("Task.sleep(nanoseconds:)") {
185-
try await Task.sleep(nanoseconds: 1_000_000_000)
192+
let diff = try await measure {
193+
try await Task.sleep(nanoseconds: 100_000_000)
194+
}
195+
try expectGTE(diff, 100)
196+
}
197+
198+
#if compiler(>=5.7)
199+
try await asyncTest("ContinuousClock.sleep") {
200+
let diff = try await measure {
201+
let c = ContinuousClock()
202+
try await c.sleep(until: .now + .milliseconds(100))
203+
}
204+
try expectGTE(diff, 99)
205+
}
206+
try await asyncTest("SuspendingClock.sleep") {
207+
let diff = try await measure {
208+
let c = SuspendingClock()
209+
try await c.sleep(until: .now + .milliseconds(100))
210+
}
211+
try expectGTE(diff, 99)
186212
}
213+
#endif
187214
}
188215

189216

Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
102102
}
103103
swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast(swift_task_enqueueGlobalWithDelay_hook_impl, to: UnsafeMutableRawPointer?.self)
104104

105+
#if compiler(>=5.7)
106+
typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) (Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original) -> Void
107+
let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = { sec, nsec, tsec, tnsec, clock, job, original in
108+
JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock)
109+
}
110+
swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast(swift_task_enqueueGlobalWithDeadline_hook_impl, to: UnsafeMutableRawPointer?.self)
111+
#endif
112+
105113
typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueMainExecutor_original) -> Void
106114
let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in
107115
JavaScriptEventLoop.shared.enqueue(job)
@@ -127,6 +135,30 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
127135
}
128136
}
129137

138+
#if compiler(>=5.7)
139+
/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88
140+
@_silgen_name("swift_get_time")
141+
internal func swift_get_time(
142+
_ seconds: UnsafeMutablePointer<Int64>,
143+
_ nanoseconds: UnsafeMutablePointer<Int64>,
144+
_ clock: CInt)
145+
146+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
147+
extension JavaScriptEventLoop {
148+
fileprivate func enqueue(
149+
_ job: UnownedJob, withDelay seconds: Int64, _ nanoseconds: Int64,
150+
_ toleranceSec: Int64, _ toleranceNSec: Int64,
151+
_ clock: Int32
152+
) {
153+
var nowSec: Int64 = 0
154+
var nowNSec: Int64 = 0
155+
swift_get_time(&nowSec, &nowNSec, clock)
156+
let delayNanosec = (seconds - nowSec) * 1_000_000_000 + (nanoseconds - nowNSec)
157+
enqueue(job, withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec))
158+
}
159+
}
160+
#endif
161+
130162
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
131163
public extension JSPromise {
132164
/// Wait for the promise to complete, returning (or throwing) its result.

Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDelay_original)(
3535
SWIFT_EXPORT_FROM(swift_Concurrency)
3636
void *_Nullable swift_task_enqueueGlobalWithDelay_hook;
3737

38-
unsigned long long foo;
38+
typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)(
39+
long long sec,
40+
long long nsec,
41+
long long tsec,
42+
long long tnsec,
43+
int clock, Job *_Nonnull job);
44+
SWIFT_EXPORT_FROM(swift_Concurrency)
45+
void *_Nullable swift_task_enqueueGlobalWithDeadline_hook;
3946

4047
/// A hook to take over main executor enqueueing.
4148
typedef SWIFT_CC(swift) void (*swift_task_enqueueMainExecutor_original)(

0 commit comments

Comments
 (0)