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