Skip to content

Commit ab55da5

Browse files
committed
move locklessCancellation to nonWebTest
It uses sleep to test deadlocks
1 parent f63f9db commit ab55da5

File tree

8 files changed

+213
-33
lines changed

8 files changed

+213
-33
lines changed

compose/runtime/runtime/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
269269
desktopTest {
270270
dependsOn(jvmTest)
271271
}
272+
nonWebTest.dependsOn(commonTest)
273+
jvmTest.dependsOn(nonWebTest)
274+
nativeTest.dependsOn(nonWebTest)
272275
webTest {
273276
dependsOn(jbTest)
274277
dependsOn(nonEmulatorCommonTest)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.compose.runtime.internal
18+
19+
import kotlinx.cinterop.ExperimentalForeignApi
20+
import kotlinx.cinterop.UnsafeNumber
21+
import kotlinx.cinterop.cValue
22+
import platform.posix.nanosleep
23+
import platform.posix.timespec
24+
25+
@OptIn(ExperimentalForeignApi::class, UnsafeNumber::class)
26+
internal actual fun sleep(timeMillis: Int) {
27+
val time = cValue<timespec> {
28+
tv_sec = timeMillis / 1000
29+
tv_nsec = (timeMillis.mod(1000) * NanoSecondsPerMilliSecond)
30+
}
31+
32+
nanosleep(time, null)
33+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.compose.runtime.internal
18+
19+
internal actual fun sleep(timeMillis: Int) {
20+
Thread.sleep(timeMillis.toLong())
21+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.compose.runtime.internal
18+
19+
import kotlinx.cinterop.ExperimentalForeignApi
20+
import kotlinx.cinterop.UnsafeNumber
21+
import kotlinx.cinterop.cValue
22+
import platform.posix.nanosleep
23+
import platform.posix.timespec
24+
25+
@OptIn(ExperimentalForeignApi::class, UnsafeNumber::class)
26+
internal actual fun sleep(timeMillis: Int) {
27+
val time = cValue<timespec> {
28+
tv_sec = (timeMillis / 1000).toLong()
29+
tv_nsec = timeMillis.mod(1000L) * NanoSecondsPerMilliSecond
30+
}
31+
32+
nanosleep(time, null)
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.compose.runtime.internal
18+
19+
import kotlinx.cinterop.ExperimentalForeignApi
20+
import kotlinx.cinterop.UnsafeNumber
21+
import kotlinx.cinterop.cValue
22+
import platform.posix.nanosleep
23+
import platform.posix.timespec
24+
25+
@OptIn(ExperimentalForeignApi::class, UnsafeNumber::class)
26+
internal actual fun sleep(timeMillis: Int) {
27+
val time = cValue<timespec> {
28+
tv_sec = (timeMillis / 1000).toLong()
29+
tv_nsec = (timeMillis.mod(1000L) * NanoSecondsPerMilliSecond).toInt()
30+
}
31+
32+
nanosleep(time, null)
33+
}

compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/BroadcastFrameClockTest.kt

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import kotlin.test.Test
2121
import kotlin.test.assertEquals
2222
import kotlin.test.assertFalse
2323
import kotlin.test.assertTrue
24+
import kotlin.time.Duration.Companion.milliseconds
2425
import kotlinx.coroutines.CancellationException
2526
import kotlinx.coroutines.CoroutineStart.UNDISPATCHED
2627
import kotlinx.coroutines.Deferred
@@ -29,6 +30,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
2930
import kotlinx.coroutines.InternalCoroutinesApi
3031
import kotlinx.coroutines.async
3132
import kotlinx.coroutines.cancelAndJoin
33+
import kotlinx.coroutines.delay
3234
import kotlinx.coroutines.launch
3335
import kotlinx.coroutines.test.UnconfinedTestDispatcher
3436
import kotlinx.coroutines.test.runTest
@@ -82,37 +84,4 @@ class BroadcastFrameClockTest {
8284
val lateAwaiter = async { clock.withFrameNanos { it } }
8385
assertAwaiterCancelled("lateAwaiter", lateAwaiter)
8486
}
85-
86-
@OptIn(InternalCoroutinesApi::class)
87-
@Test(timeout = 5_000)
88-
fun locklessCancellation() = runTest {
89-
val clock = BroadcastFrameClock()
90-
val cancellationGate = AtomicInt(1)
91-
92-
var spin = true
93-
async(start = UNDISPATCHED) {
94-
clock.withFrameNanos {
95-
cancellationGate.add(-1)
96-
@Suppress("BanThreadSleep") while (spin) Thread.sleep(100)
97-
}
98-
}
99-
100-
val cancellingJob = async(start = UNDISPATCHED) { clock.withFrameNanos {} }
101-
102-
launch(Dispatchers.Default) { clock.sendFrame(1) }
103-
104-
// Wait for the spinlock to start
105-
while (cancellationGate.get() != 0) yield()
106-
107-
// Assert that this line doesn't deadlock.
108-
cancellingJob.cancelAndJoin()
109-
110-
// Make sure that we can queue up new jobs for subsequent frames
111-
spin = false
112-
assertFalse(clock.hasAwaiters)
113-
async(start = UNDISPATCHED) { clock.withFrameNanos {} }
114-
assertTrue(clock.hasAwaiters)
115-
116-
clock.cancel()
117-
}
11887
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.compose.runtime
18+
19+
import androidx.compose.runtime.internal.AtomicInt
20+
import androidx.compose.runtime.internal.sleep
21+
import kotlin.test.Test
22+
import kotlin.test.assertFalse
23+
import kotlin.test.assertTrue
24+
import kotlin.time.Duration.Companion.milliseconds
25+
import kotlinx.coroutines.CoroutineStart.UNDISPATCHED
26+
import kotlinx.coroutines.Dispatchers
27+
import kotlinx.coroutines.ExperimentalCoroutinesApi
28+
import kotlinx.coroutines.async
29+
import kotlinx.coroutines.cancelAndJoin
30+
import kotlinx.coroutines.launch
31+
import kotlinx.coroutines.test.runTest
32+
import kotlinx.coroutines.yield
33+
34+
@ExperimentalCoroutinesApi
35+
class BroadcastFrameClockNonWebTest {
36+
@Test
37+
fun locklessCancellation() = runTest(timeout = 5_000.milliseconds) {
38+
val clock = BroadcastFrameClock()
39+
val cancellationGate = AtomicInt(1)
40+
41+
var spin = true
42+
async(start = UNDISPATCHED) {
43+
clock.withFrameNanos {
44+
cancellationGate.add(-1)
45+
@Suppress("BanThreadSleep") while (spin) { sleep(100) }
46+
}
47+
}
48+
49+
val cancellingJob = async(start = UNDISPATCHED) { clock.withFrameNanos {} }
50+
51+
launch(Dispatchers.Default) { clock.sendFrame(1) }
52+
53+
// Wait for the spinlock to start
54+
while (cancellationGate.get() != 0) yield()
55+
56+
// Assert that this line doesn't deadlock.
57+
cancellingJob.cancelAndJoin()
58+
59+
// Make sure that we can queue up new jobs for subsequent frames
60+
spin = false
61+
assertFalse(clock.hasAwaiters)
62+
async(start = UNDISPATCHED) { clock.withFrameNanos {} }
63+
assertTrue(clock.hasAwaiters)
64+
65+
clock.cancel()
66+
}
67+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.compose.runtime.internal
18+
19+
internal const val NanoSecondsPerMilliSecond = 1_000_000
20+
21+
internal expect fun sleep(timeMillis: Int)

0 commit comments

Comments
 (0)