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