|
66 | 66 | } |
67 | 67 | } |
68 | 68 |
|
| 69 | + /// When the client's sends ran and finished and the server got the |
| 70 | + /// frames, timed off the main actor, which the client's writer runs on. |
| 71 | + struct FrameTimes: Sendable { |
| 72 | + let start = ContinuousClock.now |
| 73 | + var started = 0 |
| 74 | + var sent = 0 |
| 75 | + var failures: [String] = [] |
| 76 | + var lastSent: Duration = .zero |
| 77 | + var slowestSend: Duration = .zero |
| 78 | + var received = 0 |
| 79 | + var lastReceived: Duration = .zero |
| 80 | + var mainActorWait: Duration = .zero |
| 81 | + var mainActorWaitAt: Duration = .zero |
| 82 | + |
| 83 | + var report: String { |
| 84 | + func ms(_ d: Duration) -> Int { Int(d / .milliseconds(1)) } |
| 85 | + return """ |
| 86 | + The server got \(received) of 200 frames, the last at \(ms(lastReceived)) ms. \ |
| 87 | + The client started \(started) sends and finished \(sent), the last at \ |
| 88 | + \(ms(lastSent)) ms and the slowest in \(ms(slowestSend)) ms; failed: \(failures). \ |
| 89 | + The main actor's longest wait was \(ms(mainActorWait)) ms, at \(ms(mainActorWaitAt)) ms. |
| 90 | + """ |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + struct TimedTransport: Transport { |
| 95 | + let base: any Transport |
| 96 | + let times: Locked<FrameTimes> |
| 97 | + |
| 98 | + func connect(url: String, secProtocol: String?) async throws -> any Socket { |
| 99 | + TimedSocket(base: try await base.connect(url: url, secProtocol: secProtocol), times: times) |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + final class TimedSocket: Socket { |
| 104 | + let base: any Socket |
| 105 | + let times: Locked<FrameTimes> |
| 106 | + |
| 107 | + init(base: any Socket, times: Locked<FrameTimes>) { |
| 108 | + self.base = base |
| 109 | + self.times = times |
| 110 | + } |
| 111 | + |
| 112 | + func send(_ message: SocketMessage) async throws { |
| 113 | + let began = ContinuousClock.now |
| 114 | + times.withLock { $0.started += 1 } |
| 115 | + do { |
| 116 | + try await base.send(message) |
| 117 | + } catch { |
| 118 | + times.withLock { $0.failures.append("\(error)") } |
| 119 | + throw error |
| 120 | + } |
| 121 | + let now = ContinuousClock.now |
| 122 | + times.withLock { t in |
| 123 | + t.sent += 1 |
| 124 | + t.lastSent = now - t.start |
| 125 | + t.slowestSend = max(t.slowestSend, now - began) |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + func receive() async throws -> SocketMessage? { try await base.receive() } |
| 130 | + |
| 131 | + func close(code: Int, reason: String) async { await base.close(code: code, reason: reason) } |
| 132 | + } |
| 133 | + |
69 | 134 | @Test @MainActor func keepsTheOrderOfFramesSentBeforeAndAfterTheOpen() async throws { |
70 | 135 | let server = try WebSocketServer() |
71 | 136 | let port = try await server.start() |
72 | 137 | defer { server.stop() } |
73 | | - var events = server.events.makeAsyncIterator() |
74 | 138 |
|
| 139 | + let times = Locked(FrameTimes()) |
75 | 140 | let recorder = Recorder() |
76 | | - let factory = TransportSocketFactory(URLSessionTransport()) |
| 141 | + let factory = TransportSocketFactory( |
| 142 | + TimedTransport(base: URLSessionTransport(), times: times)) |
77 | 143 | let connection = factory.open( |
78 | 144 | url: "ws://127.0.0.1:\(port)/", secProtocol: nil, events: recorder) |
79 | 145 | for i in 0..<200 { connection.send("\(i)") } |
80 | | - guard case .connected(let peer, _)? = await events.next() else { |
| 146 | + |
| 147 | + // Off the main actor, so that a busy main actor doesn't delay when the |
| 148 | + // frames are counted. |
| 149 | + let events = server.events |
| 150 | + let reading = Task.detached { () -> (peer: WebSocketServer.Peer, texts: [String])? in |
| 151 | + var events = events.makeAsyncIterator() |
| 152 | + guard case .connected(let peer, _)? = await events.next() else { return nil } |
| 153 | + var texts: [String] = [] |
| 154 | + while texts.count < 200, case .message(_, .text(let text))? = await events.next() { |
| 155 | + texts.append(text) |
| 156 | + let now = ContinuousClock.now |
| 157 | + times.withLock { t in |
| 158 | + t.received += 1 |
| 159 | + t.lastReceived = now - t.start |
| 160 | + } |
| 161 | + } |
| 162 | + return (peer, texts) |
| 163 | + } |
| 164 | + let probe = Task.detached { |
| 165 | + while !Task.isCancelled { |
| 166 | + let asked = ContinuousClock.now |
| 167 | + await MainActor.run {} |
| 168 | + let waited = ContinuousClock.now - asked |
| 169 | + times.withLock { t in |
| 170 | + if waited > t.mainActorWait { |
| 171 | + t.mainActorWait = waited |
| 172 | + t.mainActorWaitAt = asked - t.start |
| 173 | + } |
| 174 | + } |
| 175 | + try? await Task.sleep(for: .milliseconds(50)) |
| 176 | + } |
| 177 | + } |
| 178 | + let watchdog = Task { |
| 179 | + try await Task.sleep(for: .seconds(30)) |
| 180 | + Issue.record("After 30 s: \(times.withLock { $0.report })") |
| 181 | + } |
| 182 | + defer { |
| 183 | + probe.cancel() |
| 184 | + watchdog.cancel() |
| 185 | + } |
| 186 | + guard let read = await reading.value else { |
81 | 187 | Issue.record("not connected") |
82 | 188 | return |
83 | 189 | } |
84 | | - var texts: [String] = [] |
85 | | - while texts.count < 200, case .message(_, .text(let text))? = await events.next() { |
86 | | - texts.append(text) |
87 | | - } |
88 | | - #expect(texts == (0..<200).map(String.init)) |
| 190 | + #expect(read.texts == (0..<200).map(String.init)) |
89 | 191 | #expect(recorder.opened) |
90 | 192 |
|
91 | | - peer.close(code: 4003, reason: "done") |
| 193 | + read.peer.close(code: 4003, reason: "done") |
92 | 194 | while recorder.closed == nil { try await Task.sleep(for: .milliseconds(10)) } |
93 | 195 | #expect(recorder.closed == SocketClosed(code: 4003, reason: "done", wasClean: true)) |
94 | 196 | } |
95 | 197 |
|
| 198 | + @Test @MainActor func sendsWhileTheMainActorIsBusy() async throws { |
| 199 | + let server = try WebSocketServer() |
| 200 | + let port = try await server.start() |
| 201 | + defer { server.stop() } |
| 202 | + let events = server.events |
| 203 | + let received = Locked(0) |
| 204 | + let reading = Task.detached { |
| 205 | + for await event in events { |
| 206 | + guard case .message = event else { continue } |
| 207 | + received.withLock { $0 += 1 } |
| 208 | + } |
| 209 | + } |
| 210 | + defer { reading.cancel() } |
| 211 | + |
| 212 | + let recorder = Recorder() |
| 213 | + let connection = TransportSocketFactory(URLSessionTransport()).open( |
| 214 | + url: "ws://127.0.0.1:\(port)/", secProtocol: nil, events: recorder) |
| 215 | + defer { connection.close(code: nil) } |
| 216 | + while !recorder.opened { try await Task.sleep(for: .milliseconds(10)) } |
| 217 | + |
| 218 | + for i in 0..<200 { connection.send("\(i)") } |
| 219 | + // The main actor stays busy; the frames still go out. |
| 220 | + usleep(1_000_000) |
| 221 | + #expect(received.withLock { $0 } == 200) |
| 222 | + } |
| 223 | + |
96 | 224 | @Test @MainActor func connectsAClientThroughTheDefaultFactory() async throws { |
97 | 225 | let server = try WebSocketServer() |
98 | 226 | let port = try await server.start() |
|
0 commit comments