1 /* <lambda>null2 * Copyright (C) 2024 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 @file:OptIn(ExperimentalCoroutinesApi::class) 18 19 package com.android.test.tracing.coroutines 20 21 import android.platform.test.annotations.EnableFlags 22 import com.android.app.tracing.coroutines.TraceContextElement 23 import com.android.app.tracing.coroutines.TraceData 24 import com.android.app.tracing.coroutines.TraceStorage 25 import com.android.app.tracing.coroutines.createCoroutineTracingContext 26 import com.android.app.tracing.coroutines.launchTraced 27 import com.android.app.tracing.coroutines.traceCoroutine 28 import com.android.app.tracing.coroutines.traceThreadLocal 29 import com.android.systemui.Flags.FLAG_COROUTINE_TRACING 30 import java.util.concurrent.CyclicBarrier 31 import java.util.concurrent.Executors 32 import java.util.concurrent.TimeUnit 33 import kotlin.coroutines.CoroutineContext 34 import kotlin.coroutines.EmptyCoroutineContext 35 import kotlinx.coroutines.ExperimentalCoroutinesApi 36 import kotlinx.coroutines.channels.Channel 37 import kotlinx.coroutines.currentCoroutineContext 38 import kotlinx.coroutines.launch 39 import org.junit.Assert.assertArrayEquals 40 import org.junit.Assert.assertNotNull 41 import org.junit.Assert.assertNotSame 42 import org.junit.Assert.assertNull 43 import org.junit.Assert.assertSame 44 import org.junit.Assert.assertThrows 45 import org.junit.Test 46 47 @EnableFlags(FLAG_COROUTINE_TRACING) 48 class CoroutineTracingMachineryTest : TestBase() { 49 50 override val extraContext: CoroutineContext by lazy { EmptyCoroutineContext } 51 52 @Test 53 fun missingTraceContextObjects() = runTest { 54 val channel = Channel<Int>() 55 val context1 = bgThread1 56 val context2 = bgThread2 + createCoroutineTracingContext("main", testMode = true) 57 58 launchTraced("launch#1", context1) { 59 expect() 60 channel.receive() 61 traceCoroutine("span-1") { expect() } 62 expect() 63 launchTraced("launch#2", context2) { 64 // "launch#2" is not traced because TraceContextElement was installed too 65 // late; it is not part of the scope that was launched (i.e., the `this` in 66 // `this.launch {}`) 67 expect("1^main") 68 channel.receive() 69 traceCoroutine("span-2") { expect("1^main", "span-2") } 70 expect("1^main") 71 launch { 72 // ...it won't appear in the child scope either because in 73 // `launchTraced("string"), it adds: 74 // `CoroutineTraceName` + `TraceContextElement`. This demonstrates why it is 75 // important to only use `TraceContextElement` in the root scope. In this case, 76 // the `TraceContextElement` overwrites the name, so the name is dropped. 77 // Tracing still works with a default, empty name, however. 78 expect("1^main:1^") 79 } 80 } 81 expect() 82 } 83 expect() 84 85 channel.send(1) 86 channel.send(2) 87 88 launch(context1) { expect() } 89 launch(context2) { expect("2^main") } 90 } 91 92 /** 93 * Tests interleaving: 94 * ``` 95 * Thread #1 | [updateThreadContext]....^ [restoreThreadContext] 96 * -------------------------------------------------------------------------------------------- 97 * Thread #2 | [updateThreadContext]...........^[restoreThreadContext] 98 * ``` 99 * 100 * This test checks for issues with concurrent modification of the trace state. For example, the 101 * test should fail if [TraceContextElement.restoreThreadContext] uses the size of the slices 102 * array in [TraceData] as follows instead of using the `ThreadLocal` count stored in the 103 * [TraceStorage.openSliceCount] array: 104 * ``` 105 * class TraceData { 106 * ... 107 * // BAD: 108 * fun endAllOnThread() { 109 * repeat(data.slices.size) { 110 * // THIS WOULD BE AN ERROR. If the thread is slow, the TraceData object could have been 111 * // modified by another thread, meaning `data.slices.size` would be incorrect for the 112 * // current thread. 113 * endSlice() 114 * } 115 * ... 116 * } 117 * } 118 * ``` 119 */ 120 @Test 121 fun coroutineMachinery() { 122 assertNotNull(traceThreadLocal.get()) 123 assertNull(traceThreadLocal.get()?.data) 124 125 val thread1ResumptionPoint = CyclicBarrier(2) 126 val thread1SuspensionPoint = CyclicBarrier(2) 127 128 val thread1 = Executors.newSingleThreadExecutor() 129 val thread2 = Executors.newSingleThreadExecutor() 130 val slicesForThread1 = listOf("a", "c", "e", "g") 131 val slicesForThread2 = listOf("b", "d", "f", "h") 132 var failureOnThread1: Error? = null 133 var failureOnThread2: Error? = null 134 val expectedTraceForThread1 = 135 arrayOf("main", "1:a", "2:b", "1:c", "2:d", "1:e", "2:f", "1:g") 136 137 val traceContext = 138 TraceContextElement( 139 name = "main", 140 isRoot = false, 141 countContinuations = false, 142 walkStackForDefaultNames = false, 143 shouldIgnoreClassName = { false }, 144 parentId = null, 145 inheritedTracePrefix = "", 146 coroutineDepth = -1, 147 ) 148 thread1.execute { 149 slicesForThread1.forEachIndexed { index, sliceName -> 150 try { 151 assertNotNull(traceThreadLocal.get()) 152 assertNull(traceThreadLocal.get()?.data) 153 val oldTrace = traceContext.updateThreadContext(traceContext) 154 // await() AFTER updateThreadContext, thus thread #1 always resumes the 155 // coroutine before thread #2 156 assertSame(traceThreadLocal.get()!!.data, traceContext.contextTraceData) 157 158 // coroutine body start { 159 (traceThreadLocal.get() as TraceStorage).beginCoroutineTrace("1:$sliceName") 160 161 // At the end, verify the interleaved trace sections look correct: 162 if (index == slicesForThread1.size - 1) { 163 expect(*expectedTraceForThread1) 164 } 165 166 // simulate a slow thread, wait to call restoreThreadContext until after thread 167 // A has resumed 168 thread1SuspensionPoint.await(3, TimeUnit.SECONDS) 169 Thread.sleep(500) 170 // } coroutine body end 171 172 traceContext.restoreThreadContext(EmptyCoroutineContext, oldTrace) 173 thread1ResumptionPoint.await(3, TimeUnit.SECONDS) 174 assertNotNull(traceThreadLocal.get()) 175 assertNull(traceThreadLocal.get()?.data) 176 } catch (e: Error) { 177 failureOnThread1 = e 178 } 179 } 180 } 181 182 val expectedTraceForThread2 = 183 arrayOf("main", "1:a", "2:b", "1:c", "2:d", "1:e", "2:f", "1:g", "2:h") 184 thread2.execute { 185 slicesForThread2.forEachIndexed { i, n -> 186 try { 187 assertNotNull(traceThreadLocal.get()) 188 assertNull(traceThreadLocal.get()?.data) 189 thread1SuspensionPoint.await(3, TimeUnit.SECONDS) 190 191 val oldTrace = traceContext.updateThreadContext(traceContext) 192 193 // coroutine body start { 194 (traceThreadLocal.get() as TraceStorage).beginCoroutineTrace("2:$n") 195 196 // At the end, verify the interleaved trace sections look correct: 197 if (i == slicesForThread2.size - 1) { 198 expect(*expectedTraceForThread2) 199 } 200 // } coroutine body end 201 202 traceContext.restoreThreadContext(EmptyCoroutineContext, oldTrace) 203 thread1ResumptionPoint.await(3, TimeUnit.SECONDS) 204 assertNotNull(traceThreadLocal.get()) 205 assertNull(traceThreadLocal.get()?.data) 206 } catch (e: Error) { 207 failureOnThread2 = e 208 } 209 } 210 } 211 212 thread1.shutdown() 213 thread1.awaitTermination(5, TimeUnit.SECONDS) 214 thread2.shutdown() 215 thread2.awaitTermination(5, TimeUnit.SECONDS) 216 217 assertNull("Failure executing coroutine on thread-#1.", failureOnThread1) 218 assertNull("Failure executing coroutine on thread-#2.", failureOnThread2) 219 } 220 221 @Test 222 fun traceContextIsCopied() = runTest { 223 expect() 224 val traceContext = 225 createCoroutineTracingContext("main", testMode = true) as TraceContextElement 226 // Root does not have slices: 227 assertNull(traceContext.contextTraceData) 228 launch(traceContext) { 229 // After copying during launch, root still does not have slices: 230 assertNull(traceContext.contextTraceData) 231 // However, the copied object has slices: 232 val currentTce = currentCoroutineContext()[TraceContextElement] 233 assertNotNull(currentTce) 234 assertNotNull(currentTce!!.contextTraceData) 235 assertSame(traceThreadLocal.get()!!.data, currentTce.contextTraceData) 236 // slices is lazily created, so it should not be initialized yet: 237 assertThrows(UninitializedPropertyAccessException::class.java) { 238 (traceThreadLocal.get()!!.data as TraceData).slices 239 } 240 assertThrows(UninitializedPropertyAccessException::class.java) { 241 currentTce.contextTraceData!!.slices 242 } 243 expect("1^main") 244 traceCoroutine("hello") { 245 // Not the same object because it should be copied into the current context 246 assertNotSame(traceThreadLocal.get()!!.data, traceContext.contextTraceData) 247 assertArrayEquals( 248 arrayOf("hello"), 249 (traceThreadLocal.get()!!.data as TraceData).slices.toArray(), 250 ) 251 assertNull(traceContext.contextTraceData?.slices) 252 } 253 assertNotSame(traceThreadLocal.get()!!.data, traceContext.contextTraceData) 254 // Because slices is lazily created, it will no longer be uninitialized after it was 255 // used to trace "hello", but this time it will be empty 256 assertArrayEquals( 257 arrayOf(), 258 (traceThreadLocal.get()!!.data as TraceData).slices.toArray(), 259 ) 260 assertNull(traceContext.contextTraceData?.slices) 261 expect("1^main") 262 } 263 .join() 264 expect() 265 } 266 } 267