• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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