|
15 | 15 | @_spi(DispatchAsync) import DispatchAsync
|
16 | 16 | import Testing
|
17 | 17 |
|
| 18 | +import func Foundation.sin |
| 19 | + |
| 20 | +#if !os(WASI) |
| 21 | +import class Foundation.Thread |
| 22 | +#endif |
| 23 | + |
18 | 24 | private typealias DispatchGroup = DispatchAsync.DispatchGroup
|
| 25 | +private typealias DispatchQueue = DispatchAsync.DispatchQueue |
19 | 26 |
|
20 |
| -@Test(arguments: [100]) |
21 |
| -func dispatchGroupOrderCleanliness(repetitions: Int) async throws { |
22 |
| - // Repeating this `repetitions` number of times to help rule out |
23 |
| - // edge cases that only show up some of the time |
24 |
| - for index in 0 ..< repetitions { |
25 |
| - Task { |
26 |
| - actor Result { |
27 |
| - private(set) var value = "" |
28 |
| - |
29 |
| - func append(value: String) { |
30 |
| - self.value.append(value) |
| 27 | +@Suite("DispatchGroup Tests") |
| 28 | +struct DispatchGroupTests { |
| 29 | + @Test(arguments: [1000]) |
| 30 | + @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) |
| 31 | + func dispatchGroupOrderCleanliness(repetitions: Int) async throws { |
| 32 | + // Repeating this `repetitions` number of times to help rule out |
| 33 | + // edge cases that only show up some of the time |
| 34 | + for index in 0 ..< repetitions { |
| 35 | + Task { |
| 36 | + actor Result { |
| 37 | + private(set) var value = "" |
| 38 | + |
| 39 | + func append(value: String) { |
| 40 | + self.value.append(value) |
| 41 | + } |
31 | 42 | }
|
32 |
| - } |
33 | 43 |
|
34 |
| - let result = Result() |
| 44 | + let result = Result() |
35 | 45 |
|
36 |
| - let group = DispatchGroup() |
37 |
| - await result.append(value: "|🔵\(index)") |
| 46 | + let group = DispatchGroup() |
| 47 | + await result.append(value: "|🔵\(iteration)") |
38 | 48 |
|
39 |
| - group.enter() |
40 |
| - Task { |
41 |
| - await result.append(value: "🟣/") |
42 |
| - group.leave() |
43 |
| - } |
| 49 | + group.enter() |
| 50 | + Task { |
| 51 | + await result.append(value: "🟣/") |
| 52 | + group.leave() |
| 53 | + } |
44 | 54 |
|
45 |
| - group.enter() |
46 |
| - Task { |
47 |
| - await result.append(value: "🟣^") |
48 |
| - group.leave() |
49 |
| - } |
| 55 | + group.enter() |
| 56 | + Task { |
| 57 | + await result.append(value: "🟣^") |
| 58 | + group.leave() |
| 59 | + } |
50 | 60 |
|
51 |
| - group.enter() |
52 |
| - Task { |
53 |
| - await result.append(value: "🟣\\") |
54 |
| - group.leave() |
| 61 | + group.enter() |
| 62 | + Task { |
| 63 | + await result.append(value: "🟣\\") |
| 64 | + group.leave() |
| 65 | + } |
| 66 | + |
| 67 | + await withCheckedContinuation { continuation in |
| 68 | + group.notify(queue: .main) { |
| 69 | + Task { |
| 70 | + await result.append(value: "🟢\(iteration)=") |
| 71 | + continuation.resume() |
| 72 | + } |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + let finalValue = await result.value |
| 77 | + |
| 78 | + /// NOTE: If you need to visually debug issues, you can uncomment |
| 79 | + /// the following to watch a visual representation of the group ordering. |
| 80 | + /// |
| 81 | + /// In general, you'll see something like the following printed over and over |
| 82 | + /// to the console: |
| 83 | + /// |
| 84 | + /// ``` |
| 85 | + /// |🔵42🟣/🟣^🟣\🟢42= |
| 86 | + /// ``` |
| 87 | + /// |
| 88 | + /// What you should observe: |
| 89 | + /// |
| 90 | + /// - The index number be the same at the beginning and end of each line, and it |
| 91 | + /// should always increment by one. |
| 92 | + /// - The 🔵 should always be first, and the 🟢 should always be last for each line. |
| 93 | + /// - There should always be 3 🟣's in between the 🔵 and 🟢. |
| 94 | + /// - The ordering of the 🟣 can be random, and that is fine. |
| 95 | + /// |
| 96 | + /// For example, for of the following are valid outputs: |
| 97 | + /// |
| 98 | + /// ``` |
| 99 | + /// // GOOD |
| 100 | + /// |🔵42🟣/🟣^🟣\🟢42= |
| 101 | + /// ``` |
| 102 | + /// |
| 103 | + /// ``` |
| 104 | + /// // GOOD |
| 105 | + /// |🔵42🟣/🟣\🟣^🟢42= |
| 106 | + /// ``` |
| 107 | + /// |
| 108 | + /// But the following would not be valid: |
| 109 | + /// |
| 110 | + /// ``` |
| 111 | + /// // BAD! (43 comes before 42) |
| 112 | + /// |🔵43🟣/🟣^🟣\🟢43= |
| 113 | + /// |🔵42🟣/🟣^🟣\🟢42= |
| 114 | + /// |🔵44🟣/🟣^🟣\🟢44= |
| 115 | + /// ``` |
| 116 | + /// |
| 117 | + /// ``` |
| 118 | + /// // BAD! (green globe comes before a purle one) |
| 119 | + /// |🔵42🟣/🟣^🟢42🟣\= |
| 120 | + /// ``` |
| 121 | + /// |
| 122 | + |
| 123 | + // NOTE: Uncomment to use troubleshooting method above: |
| 124 | + // print(finalValue) |
| 125 | + |
| 126 | + #expect(finalValue.prefix(1) == "|") |
| 127 | + #expect(finalValue.count { $0 == "🟣" } == 3) |
| 128 | + #expect(finalValue.count { $0 == "🟢" } == 1) |
| 129 | + #expect(finalValue.lastIndex(of: "🟣")! < finalValue.firstIndex(of: "🟢")!) |
| 130 | + #expect(finalValue.suffix(1) == "=") |
55 | 131 | }
|
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + /// Swift port of libdispatch/tests/dispatch_group.c |
| 136 | + /// |
| 137 | + /// See https://github.com/swiftlang/swift-corelibs-libdispatch/blob/686475721aca13d98d2eab3a0c439403d33b6e2d/tests/dispatch_group.c |
| 138 | + /// |
| 139 | + /// The original C test stresses `dispatch_group_wait` by enqueuing a bunch of |
| 140 | + /// math-heavy blocks on a global queue, then waiting for them to finish with a |
| 141 | + /// timeout. It also verifies that `notify` is invoked exactly once. |
| 142 | + @Test(.timeLimit(.minutes(1))) |
| 143 | + @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) |
| 144 | + func dispatchGroupStress() async throws { |
| 145 | + let iterations = 1000 |
| 146 | + // We use a separate concurrent queue rather than the global queue to avoid interference issues |
| 147 | + // with other tests running in parallel |
| 148 | + let workQueue = DispatchQueue(attributes: .concurrent) |
| 149 | + let group = DispatchGroup() |
56 | 150 |
|
57 |
| - await withCheckedContinuation { continuation in |
58 |
| - group.notify(queue: .main) { |
59 |
| - Task { |
60 |
| - await result.append(value: "🟢\(index)=") |
61 |
| - continuation.resume() |
| 151 | + let isolationQueue = DispatchQueue(label: "isolationQueue") |
| 152 | + nonisolated(unsafe) var counter = 0 |
| 153 | + |
| 154 | + for _ in 0 ..< iterations { |
| 155 | + group.enter() |
| 156 | + workQueue.async { |
| 157 | + // We alternate between two options for workload. One is a simple |
| 158 | + // math function, the other is a thread sleep. |
| 159 | + // |
| 160 | + // Alternating between those two approaches provides variance to |
| 161 | + // increases failure chances if there are race conditions subject to timing |
| 162 | + // and load. |
| 163 | + if Bool.random() { |
| 164 | + #if !os(WASI) |
| 165 | + Thread.sleep(forTimeInterval: 0.00001) // 10_000 nanoseconds |
| 166 | + #endif |
| 167 | + } else { |
| 168 | + // A small math workload similar to the original C test which used |
| 169 | + // sin(random()). We iterate a couple thousand times to keep the CPU |
| 170 | + // busy long enough for the group scheduling to matter. |
| 171 | + var x = Double.random(in: 0.0 ... Double.pi) |
| 172 | + for _ in 0 ..< 2_000 { |
| 173 | + x = sin(x) |
62 | 174 | }
|
63 | 175 | }
|
| 176 | + |
| 177 | + isolationQueue.async { |
| 178 | + counter += 1 |
| 179 | + group.leave() |
| 180 | + } |
64 | 181 | }
|
| 182 | + } |
| 183 | + |
| 184 | + // NOTE: The test has a 1 minute time limit that will time out. In |
| 185 | + // the original code, this timeout was 5 seconds, but currently |
| 186 | + // the shortest timeout Swift Testing provides is 1 minute. |
| 187 | + await group.wait() |
65 | 188 |
|
66 |
| - let finalValue = await result.value |
67 |
| - |
68 |
| - /// NOTE: If you need to visually debug issues, you can uncomment |
69 |
| - /// the following to watch a visual representation of the group ordering. |
70 |
| - /// |
71 |
| - /// In general, you'll see something like the following printed over and over |
72 |
| - /// to the console: |
73 |
| - /// |
74 |
| - /// ``` |
75 |
| - /// |🔵42🟣/🟣^🟣\🟢42= |
76 |
| - /// ``` |
77 |
| - /// |
78 |
| - /// What you should observe: |
79 |
| - /// |
80 |
| - /// - The index number be the same at the beginning and end of each line, and it |
81 |
| - /// should always increment by one. |
82 |
| - /// - The 🔵 should always be first, and the 🟢 should always be last for each line. |
83 |
| - /// - There should always be 3 🟣's in between the 🔵 and 🟢. |
84 |
| - /// - The ordering of the 🟣 can be random, and that is fine. |
85 |
| - /// |
86 |
| - /// For example, for of the following are valid outputs: |
87 |
| - /// |
88 |
| - /// ``` |
89 |
| - /// // GOOD |
90 |
| - /// |🔵42🟣/🟣^🟣\🟢42= |
91 |
| - /// ``` |
92 |
| - /// |
93 |
| - /// ``` |
94 |
| - /// // GOOD |
95 |
| - /// |🔵42🟣/🟣\🟣^🟢42= |
96 |
| - /// ``` |
97 |
| - /// |
98 |
| - /// But the following would not be valid: |
99 |
| - /// |
100 |
| - /// ``` |
101 |
| - /// // BAD! |
102 |
| - /// |🔵43🟣/🟣^🟣\🟢43= |
103 |
| - /// |🔵42🟣/🟣^🟣\🟢42= |
104 |
| - /// |🔵44🟣/🟣^🟣\🟢44= |
105 |
| - /// ``` |
106 |
| - /// |
107 |
| - /// ``` |
108 |
| - /// // BAD! |
109 |
| - /// |🔵42🟣/🟣^🟢42🟣\= |
110 |
| - /// ``` |
111 |
| - /// |
112 |
| - |
113 |
| - // Uncomment to use troubleshooting method above: |
114 |
| - // print(finalValue) |
115 |
| - |
116 |
| - #expect(finalValue.prefix(1) == "|") |
117 |
| - #expect(finalValue.count { $0 == "🟣" } == 3) |
118 |
| - #expect(finalValue.count { $0 == "🟢" } == 1) |
119 |
| - #expect(finalValue.lastIndex(of: "🟣")! < finalValue.firstIndex(of: "🟢")!) |
120 |
| - #expect(finalValue.suffix(1) == "=") |
| 189 | + // Verify notify fires exactly once. |
| 190 | + nonisolated(unsafe) var notifyHits = 0 |
| 191 | + await withCheckedContinuation { k in |
| 192 | + group.notify(queue: .main) { |
| 193 | + notifyHits += 1 |
| 194 | + k.resume() |
| 195 | + } |
121 | 196 | }
|
| 197 | + #expect(notifyHits == 1) |
| 198 | + |
| 199 | + let finalCount = counter |
| 200 | + #expect(finalCount == iterations) |
122 | 201 | }
|
123 | 202 | }
|
0 commit comments