diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index 1e48f459f..c80e48779 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -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 @@ -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") { @@ -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") { @@ -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:)") { @@ -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 @@ -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 } diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 7c4a1c905..72dc8f503 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -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) @@ -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, + _ nanoseconds: UnsafeMutablePointer, + _ 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. diff --git a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h index 51c98afc6..b24d19d04 100644 --- a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h +++ b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h @@ -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)(