Skip to content

Support Clock-based sleep APIs #216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 58 additions & 31 deletions IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import Darwin

#if compiler(>=5.5)

func performanceNow() -> Double {
return JSObject.global.performance.now.function!().number!
}

func measure(_ block: () async throws -> Void) async rethrows -> Double {
let start = performanceNow()
try await block()
return performanceNow() - start
}

func entrypoint() async throws {
struct E: Error, Equatable {
let value: Int
Expand Down Expand Up @@ -61,10 +71,10 @@ func entrypoint() async throws {
}

try await asyncTest("Task.sleep(_:)") {
let start = time(nil)
try await Task.sleep(nanoseconds: 2_000_000_000)
let diff = difftime(time(nil), start);
try expectGTE(diff, 2)
let diff = try await measure {
try await Task.sleep(nanoseconds: 200_000_000)
}
try expectGTE(diff, 200)
}

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

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

let start = time(nil)
let promise = JSPromise(from: delayObject.closure!())
try expectNotNil(promise)
let result = try await promise!.value
let diff = difftime(time(nil), start)
try expectGTE(diff, 2)
try expectEqual(result, .number(3))
let diff = try await measure {
let promise = JSPromise(from: delayObject.closure!())
try expectNotNil(promise)
let result = try await promise!.value
try expectEqual(result, .number(3))
}
try expectGTE(diff, 200)
}

try await asyncTest("Async JSPromise: then") {
Expand All @@ -124,18 +134,18 @@ func entrypoint() async throws {
resolve(.success(JSValue.number(3)))
return .undefined
}.jsValue,
1_000
100
)
}
let promise2 = promise.then { result in
try await Task.sleep(nanoseconds: 1_000_000_000)
try await Task.sleep(nanoseconds: 100_000_000)
return String(result.number!)
}
let start = time(nil)
let result = try await promise2.value
let diff = difftime(time(nil), start)
try expectGTE(diff, 2)
try expectEqual(result, .string("3.0"))
let diff = try await measure {
let result = try await promise2.value
try expectEqual(result, .string("3.0"))
}
try expectGTE(diff, 200)
}

try await asyncTest("Async JSPromise: then(success:failure:)") {
Expand All @@ -145,7 +155,7 @@ func entrypoint() async throws {
resolve(.failure(JSError(message: "test").jsValue))
return .undefined
}.jsValue,
1_000
100
)
}
let promise2 = promise.then { _ in
Expand All @@ -164,26 +174,43 @@ func entrypoint() async throws {
resolve(.failure(JSError(message: "test").jsValue))
return .undefined
}.jsValue,
1_000
100
)
}
let promise2 = promise.catch { err in
try await Task.sleep(nanoseconds: 1_000_000_000)
try await Task.sleep(nanoseconds: 100_000_000)
return err
}
let start = time(nil)
let result = try await promise2.value
let diff = difftime(time(nil), start)
try expectGTE(diff, 2)
try expectEqual(result.object?.message, .string("test"))
let diff = try await measure {
let result = try await promise2.value
try expectEqual(result.object?.message, .string("test"))
}
try expectGTE(diff, 200)
}

// FIXME(katei): Somehow it doesn't work due to a mysterious unreachable inst
// at the end of thunk.
// This issue is not only on JS host environment, but also on standalone coop executor.
try await asyncTest("Task.sleep(nanoseconds:)") {
try await Task.sleep(nanoseconds: 1_000_000_000)
let diff = try await measure {
try await Task.sleep(nanoseconds: 100_000_000)
}
try expectGTE(diff, 100)
}

#if compiler(>=5.7)
try await asyncTest("ContinuousClock.sleep") {
let diff = try await measure {
let c = ContinuousClock()
try await c.sleep(until: .now + .milliseconds(100))
}
try expectGTE(diff, 99)
}
try await asyncTest("SuspendingClock.sleep") {
let diff = try await measure {
let c = SuspendingClock()
try await c.sleep(until: .now + .milliseconds(100))
}
try expectGTE(diff, 99)
}
#endif
}


Expand Down
32 changes: 32 additions & 0 deletions Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
}
swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast(swift_task_enqueueGlobalWithDelay_hook_impl, to: UnsafeMutableRawPointer?.self)

#if compiler(>=5.7)
typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) (Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original) -> Void
let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = { sec, nsec, tsec, tnsec, clock, job, original in
JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock)
}
swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast(swift_task_enqueueGlobalWithDeadline_hook_impl, to: UnsafeMutableRawPointer?.self)
#endif

typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueMainExecutor_original) -> Void
let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in
JavaScriptEventLoop.shared.enqueue(job)
Expand All @@ -127,6 +135,30 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
}
}

#if compiler(>=5.7)
/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88
@_silgen_name("swift_get_time")
internal func swift_get_time(
_ seconds: UnsafeMutablePointer<Int64>,
_ nanoseconds: UnsafeMutablePointer<Int64>,
_ clock: CInt)

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension JavaScriptEventLoop {
fileprivate func enqueue(
_ job: UnownedJob, withDelay seconds: Int64, _ nanoseconds: Int64,
_ toleranceSec: Int64, _ toleranceNSec: Int64,
_ clock: Int32
) {
var nowSec: Int64 = 0
var nowNSec: Int64 = 0
swift_get_time(&nowSec, &nowNSec, clock)
let delayNanosec = (seconds - nowSec) * 1_000_000_000 + (nanoseconds - nowNSec)
enqueue(job, withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec))
}
}
#endif

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public extension JSPromise {
/// Wait for the promise to complete, returning (or throwing) its result.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDelay_original)(
SWIFT_EXPORT_FROM(swift_Concurrency)
void *_Nullable swift_task_enqueueGlobalWithDelay_hook;

unsigned long long foo;
typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)(
long long sec,
long long nsec,
long long tsec,
long long tnsec,
int clock, Job *_Nonnull job);
SWIFT_EXPORT_FROM(swift_Concurrency)
void *_Nullable swift_task_enqueueGlobalWithDeadline_hook;

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