1 /* 2 * 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 package com.example.tracing.demo.experiments 17 18 import com.android.app.tracing.TraceUtils.traceAsync 19 import com.android.app.tracing.coroutines.createCoroutineTracingContext 20 import com.android.app.tracing.coroutines.launchTraced 21 import com.example.tracing.demo.FixedThread1 22 import javax.inject.Inject 23 import javax.inject.Singleton 24 import kotlin.contracts.ExperimentalContracts 25 import kotlin.contracts.InvocationKind 26 import kotlin.contracts.contract 27 import kotlin.coroutines.EmptyCoroutineContext 28 import kotlin.coroutines.coroutineContext 29 import kotlinx.coroutines.CoroutineDispatcher 30 import kotlinx.coroutines.CoroutineScope 31 import kotlinx.coroutines.Job 32 import kotlinx.coroutines.delay 33 import kotlinx.coroutines.job 34 import kotlinx.coroutines.launch 35 36 @Singleton 37 class BasicTracingTutorial 38 @Inject 39 constructor(@FixedThread1 private var handlerDispatcher: CoroutineDispatcher) : Experiment() { 40 41 override val description: String = "Basic tracing tutorial" 42 43 @OptIn(ExperimentalContracts::class) runStepnull44 private suspend inline fun runStep(stepNumber: Int = 0, crossinline block: (Job) -> Unit) { 45 contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } 46 traceAsync(TRACK_NAME, "Step #$stepNumber") { block(coroutineContext.job) } 47 traceAsync(TRACK_NAME, "cooldown") { delay(10) } 48 } 49 50 /** 1: Untraced coroutine on default dispatcher */ step1UntracedCoroutineOnDefaultDispatchernull51 private fun step1UntracedCoroutineOnDefaultDispatcher(job: Job) { 52 // First, we will start with a basic coroutine that has no tracing: 53 val scope = CoroutineScope(job + EmptyCoroutineContext) 54 scope.launch { delay(1) } 55 56 /* 57 Expected trace output (image alt text): 58 Trace showing a coroutine launched on the default dispatcher. The thread runs, 59 then stops, and then runs again 60 61 There is not much useful information in the trace. We see the thread runs, then 62 stops running due to the `delay(1)` call, then runs again after the delay. 63 */ 64 } 65 66 /** 2: Untraced coroutine on traced Looper thread */ step2UntracedCoroutineOnTracedLooperThreadnull67 private fun step2UntracedCoroutineOnTracedLooperThread(job: Job) { 68 /* 69 Next, we will switch from the default dispatcher to a single-threaded dispatcher 70 backed by an Android `Looper`. We will also set a trace tag for the `Looper` so 71 that the `Runnable` class names appear in the trace: 72 Next, we'll launch a coroutine with a delay: 73 */ 74 val scope = CoroutineScope(job + handlerDispatcher) 75 scope.launch { delay(1) } 76 77 /* 78 Expected trace output (image alt text): 79 Trace showing a coroutine launched on the "demo-main-thread" thread. The 80 trace shows trace sections with the names `android.os.Handler: 81 kotlinx.coroutines.internal.DispatchedContinuation` and `android.os.Handler: 82 kotlinx.coroutines.android.HandlerContext$scheduleResumeAfterDelay$$inlined$Runnable$1`. 83 This is better; we now trace sections for `android.os.Handler: 84 kotlinx.coroutines.internal.DispatchedContinuation` and `android.os.Handler: 85 kotlinx.coroutines.android.HandlerContext$scheduleResumeAfterDelay$$inlined$Runnable$1`, 86 but this still does not give us much useful information. 87 */ 88 } 89 90 /** 3: Replacing `delay` with `forceSuspend` */ step3ReplaceDelayWithForceSuspendnull91 private fun step3ReplaceDelayWithForceSuspend(job: Job) { 92 /* 93 Next, for clarity, we will replace `delay()` with our own implementation called 94 `forceSuspend`. 95 96 `forceSuspend` is similar to `delay` except that it is guaranteed to always 97 suspend. It also has backed by a `Looper`, and it emits trace sections for 98 demonstration purposes. We will also pass it a tag, "A", to make our call 99 identifiable in the trace later. 100 */ 101 val scope = CoroutineScope(job + handlerDispatcher) 102 scope.launch { forceSuspend("A", 1) } 103 104 /* 105 Expected trace output (image alt text): 106 Trace showing a coroutine launched on a handler dispatcher. The trace shows 107 the 108 kotlinx.coroutines.android.HandlerContext$scheduleResumeAfterDelay$$inlined$Runnable$1 109 coroutine 110 111 We see a trace section when `forceSuspend` schedules a `Runnable` on the 112 `Handler`, and later there is a trace section when the `Runnable` resumes the 113 continuation. 114 */ 115 } 116 117 /** 4: Coroutine with `TraceContextElement` installed */ step4CoroutineWithTraceContextElementnull118 private fun step4CoroutineWithTraceContextElement(job: Job) { 119 // Next, we'll install a `TraceContextElement` to the top-level coroutine: 120 val scope = CoroutineScope(job + handlerDispatcher + createCoroutineTracingContext()) 121 scope.launch { forceSuspend("A", 1) } 122 123 /* 124 Expected trace output (image alt text): 125 Trace showing a coroutine launched on a handler dispatcher with a 126 `TraceContextElement` installed 127 128 A new trace section named `coroutine execution` appears. Underneath it, an 129 additional slice contains metadata, which in this case looks like: 130 `;d=1;c=988384889;p=1577051477` 131 132 The string before the first semicolon (`;`) is the name of the resumed 133 coroutine. In the above example, no name was given to `launch`, and the tracing 134 context was not created with options to automatically infer a name, so the name 135 is blank. 136 137 The other fields are as follows: 138 139 * `d=` is the depth, or how many parent coroutines there are until we reach 140 the top-level coroutine. 141 * `c=` is the ID of the current coroutine. 142 * `p=` is the ID of the parent coroutine. 143 144 Thus, in the above example slice, if we want to find slices belonging to the 145 parent coroutine, we would search the trace for `;c=1577051477;`. 146 147 Note: The parent coroutine will only be included in the Perfetto trace if it 148 happens to run sometime during when the trace was captured. Parent coroutines 149 may run in parallel to their children, so it is not necessarily the case that 150 the child has to be created *after* tracing has started to know the parent name 151 (although that helps). 152 153 In the above trace, we also see `delay(1) ["A"]` is now traced as well. That's 154 because `forceSuspend()` calls `traceCoroutine("forceSuspend") {}`. 155 156 `traceCoroutine("[trace-name]") { }` can be used for tracing sections of 157 suspending code. The trace name will start and end as the coroutine suspends and 158 resumes. 159 */ 160 } 161 162 /** 5: Enable `walkStackForDefaultNames` */ step5EnableStackWalkernull163 private fun step5EnableStackWalker(job: Job) { 164 // Next, we'll enable `walkStackForDefaultNames`: 165 val scope = 166 CoroutineScope( 167 job + 168 handlerDispatcher + 169 createCoroutineTracingContext(walkStackForDefaultNames = true) 170 ) 171 scope.launch { forceSuspend("A", 1) } 172 173 /* 174 Expected trace output (image alt text): 175 Trace showing a coroutine launched on a handler dispatcher with 176 `walkStackForDefaultNames` enabled. The trace shows that `launch` has the name 177 `BasicTracingTutorial.step5EnableStackWalker;d=1;c=1560424941;p=1105235868` 178 179 Now, we can see our coroutine has a name: 180 `BasicTracingTutorial.step5EnableStackWalker;d=1;c=1560424941;p=1105235868`. 181 182 We can also see a slice named `walkStackForClassName` that occurs before the 183 `launch`. This is where the coroutine trace context infers the name of the 184 newly launched coroutine by inspecting the stack trace to see where it was 185 created. 186 187 One downside of using `walkStackForDefaultNames = true` is that it is expensive, 188 sometimes taking longer than 1 millisecond to infer the name of a class, 189 so it should be used sparingly. As we'll see further on, some parts of 190 `kotlinx.coroutines` are written such that there is no way for us to insert a 191 custom coroutine context, thus making `walkStackForClassName` unavoidable. 192 */ 193 } 194 195 /** 6: Replace `launch` with `launchTraced` */ step6UseLaunchedTracednull196 private fun step6UseLaunchedTraced(job: Job) { 197 // Walking the stack is an expensive operation, so next we'll replace our call to 198 // `launch` with `launchTraced`: 199 val scope = 200 CoroutineScope( 201 job + 202 handlerDispatcher + 203 createCoroutineTracingContext(walkStackForDefaultNames = true) 204 ) 205 scope.launchTraced { forceSuspend("A", 1) } 206 /* 207 Expected trace output (image alt text): 208 Trace showing a coroutine launched on a handler dispatcher with `launchTraced` instead of 209 `launch` 210 211 Now we see the trace section is named: 212 `BasicTracingTutorial$step6$1;d=1;c=1529321599;p=1334272881`. This is almost the 213 same as the name in the previous step, except this name is derived using the 214 classname of the supplied lambda, `block::class.simpleName`, which is much 215 faster. We also see that `walkStackForClassName` was not called. 216 */ 217 } 218 219 /** 7: Call `launchTraced` with an explicit name */ step7ExplicitLaunchNamenull220 private fun step7ExplicitLaunchName(job: Job) { 221 // Finally, we'll pass an explicit name to `launchTraced` instead of using the 222 // inline name: 223 val scope = 224 CoroutineScope( 225 job + 226 handlerDispatcher + 227 createCoroutineTracingContext(walkStackForDefaultNames = true) 228 ) 229 scope.launchTraced("my-launch") { forceSuspend("A", 1) } 230 231 /* 232 Expected trace output (image alt text): 233 Trace showing a coroutine launched on a handler dispatcher with an explicit name 234 235 Now we see the trace name is: `my-launch;d=1;c=1148426666;p=1556983557`. 236 */ 237 } 238 239 /** 8: Enable `countContinuations` */ step8CountContinuationsnull240 private fun step8CountContinuations(job: Job) { 241 // The config parameter `countContinuations` can be used to count how many times a 242 // coroutine has run, in total, since its creation: 243 val scope = 244 CoroutineScope( 245 job + 246 handlerDispatcher + 247 createCoroutineTracingContext( 248 walkStackForDefaultNames = true, 249 countContinuations = true, 250 ) 251 ) 252 scope.launchTraced("my-launch") { 253 forceSuspend("A", 1) 254 forceSuspend("B", 1) 255 forceSuspend("C", 1) 256 } 257 /* 258 Expected trace output (image alt text): 259 Trace showing a coroutine resuming after a delay, in which the continuation 260 counter has incremented to 3 261 262 In the above trace, the coroutine suspends 3 times. The counter is `0` for the 263 first resumption, and the last resumption is `3`. 264 */ 265 } 266 runExperimentnull267 override suspend fun runExperiment() { 268 runStep(1, ::step1UntracedCoroutineOnDefaultDispatcher) 269 runStep(2, ::step2UntracedCoroutineOnTracedLooperThread) 270 runStep(3, ::step3ReplaceDelayWithForceSuspend) 271 runStep(4, ::step4CoroutineWithTraceContextElement) 272 runStep(5, ::step5EnableStackWalker) 273 runStep(6, ::step6UseLaunchedTraced) 274 runStep(7, ::step7ExplicitLaunchName) 275 runStep(8, ::step8CountContinuations) 276 } 277 } 278