• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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 com.android.app.tracing
18 
19 import android.annotation.SuppressLint
20 import android.os.Trace
21 import com.android.app.tracing.TrackGroupUtils.trackGroup
22 import com.android.app.tracing.coroutines.traceCoroutine
23 import java.util.concurrent.ThreadLocalRandom
24 import kotlin.contracts.ExperimentalContracts
25 import kotlin.contracts.InvocationKind
26 import kotlin.contracts.contract
27 
28 /**
29  * Writes a trace message to indicate that a given section of code has begun running __on the
30  * current thread__. This must be followed by a corresponding call to [endSlice] in a reasonably
31  * short amount of time __on the same thread__ (i.e. _before_ the thread becomes idle again and
32  * starts running other, unrelated work).
33  *
34  * Calls to [beginSlice] and [endSlice] may be nested, and they will render in Perfetto as follows:
35  * ```
36  * Thread #1 | [==========================]
37  *           |       [==============]
38  *           |           [====]
39  * ```
40  *
41  * This function is provided for convenience to wrap a call to [Trace.traceBegin], which is more
42  * verbose to call than [Trace.beginSection], but has the added benefit of not throwing an
43  * [IllegalArgumentException] if the provided string is longer than 127 characters. We use the term
44  * "slice" instead of "section" to be consistent with Perfetto.
45  *
46  * # Avoiding malformed traces
47  *
48  * Improper usage of this API will lead to malformed traces with long slices that sometimes never
49  * end. This will look like the following:
50  * ```
51  * Thread #1 | [===================================================================== ...
52  *           |       [==============]         [====================================== ...
53  *           |           [=======]              [======]       [===================== ...
54  *           |                                                       [=======]
55  * ```
56  *
57  * To avoid this, [beginSlice] and [endSlice] should never be called from `suspend` blocks (instead,
58  * use [traceCoroutine] for tracing suspending functions). While it would be technically okay to
59  * call from a suspending function if that function were to only wrap non-suspending blocks with
60  * [beginSlice] and [endSlice], doing so is risky because suspend calls could be mistakenly added to
61  * that block as the code is refactored.
62  *
63  * Additionally, it is _not_ okay to call [beginSlice] when registering a callback and match it with
64  * a call to [endSlice] inside that callback, even if the callback runs on the same thread. Doing so
65  * would cause malformed traces because the [beginSlice] wasn't closed before the thread became idle
66  * and started running unrelated work.
67  *
68  * @param sliceName The name of the code section to appear in the trace
69  * @see endSlice
70  * @see traceCoroutine
71  */
72 @SuppressLint("UnclosedTrace")
73 @PublishedApi
beginSlicenull74 internal fun beginSlice(sliceName: String) {
75     Trace.traceBegin(Trace.TRACE_TAG_APP, sliceName)
76 }
77 
78 /**
79  * Writes a trace message to indicate that a given section of code has ended. This call must be
80  * preceded by a corresponding call to [beginSlice]. See [beginSlice] for important information
81  * regarding usage.
82  *
83  * @see beginSlice
84  * @see traceCoroutine
85  */
86 @PublishedApi
endSlicenull87 internal fun endSlice() {
88     Trace.traceEnd(Trace.TRACE_TAG_APP)
89 }
90 
91 /**
92  * Run a block within a [Trace] section. Calls [Trace.beginSection] before and [Trace.endSection]
93  * after the passed block.
94  */
95 @OptIn(ExperimentalContracts::class)
traceSectionnull96 public inline fun <T> traceSection(tag: String, block: () -> T): T {
97     contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
98     val tracingEnabled = Trace.isEnabled()
99     if (tracingEnabled) beginSlice(tag)
100     return try {
101         // Note that as this is inline, the block section would be duplicated if it is called
102         // several times. For this reason, we're using the try/finally even if tracing is disabled.
103         block()
104     } finally {
105         if (tracingEnabled) endSlice()
106     }
107 }
108 
109 /**
110  * Same as [traceSection], but the tag is provided as a lambda to help avoiding creating expensive
111  * strings when not needed.
112  */
113 @OptIn(ExperimentalContracts::class)
traceSectionnull114 public inline fun <T> traceSection(tag: () -> String, block: () -> T): T {
115     contract {
116         callsInPlace(tag, InvocationKind.AT_MOST_ONCE)
117         callsInPlace(block, InvocationKind.EXACTLY_ONCE)
118     }
119     val tracingEnabled = Trace.isEnabled()
120     if (tracingEnabled) beginSlice(tag())
121     return try {
122         block()
123     } finally {
124         if (tracingEnabled) endSlice()
125     }
126 }
127 
128 /**
129  * Like [com.android.app.tracing.traceSection], but uses `crossinline` so we don't accidentally
130  * introduce non-local returns. This is less convenient to use, but it ensures we will not
131  * accidentally pass a suspending function to this method.
132  */
133 @OptIn(ExperimentalContracts::class)
traceBlockingnull134 internal inline fun <T> traceBlocking(sectionName: String, crossinline block: () -> T): T {
135     contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
136     Trace.traceBegin(Trace.TRACE_TAG_APP, sectionName)
137     return try {
138         block()
139     } finally {
140         Trace.traceEnd(Trace.TRACE_TAG_APP)
141     }
142 }
143 
144 @OptIn(ExperimentalContracts::class)
145 public object TraceUtils {
146     public const val TAG: String = "TraceUtils"
147     public const val DEFAULT_TRACK_NAME: String = "AsyncTraces"
148 
149     @JvmStatic
tracenull150     public inline fun <T> trace(tag: () -> String, block: () -> T): T {
151         return traceSection(tag) { block() }
152     }
153 
154     @JvmStatic
tracenull155     public inline fun <T> trace(tag: String, crossinline block: () -> T): T {
156         return traceSection(tag) { block() }
157     }
158 
159     @JvmStatic
traceRunnablenull160     public inline fun traceRunnable(tag: String, crossinline block: () -> Unit): Runnable {
161         return Runnable { traceSection(tag) { block() } }
162     }
163 
164     @JvmStatic
traceRunnablenull165     public inline fun traceRunnable(
166         crossinline tag: () -> String,
167         crossinline block: () -> Unit,
168     ): Runnable {
169         return Runnable { traceSection(tag) { block() } }
170     }
171 
172     /**
173      * Creates an async slice in a track called "AsyncTraces".
174      *
175      * This can be used to trace coroutine code. Note that all usages of this method will appear
176      * under a single track.
177      */
178     @JvmStatic
traceAsyncnull179     public inline fun <T> traceAsync(method: String, block: () -> T): T =
180         traceAsync(DEFAULT_TRACK_NAME, method, block)
181 
182     /** Creates an async slice in the default track. */
183     @JvmStatic
184     public inline fun <T> traceAsync(tag: () -> String, block: () -> T): T {
185         val tracingEnabled = Trace.isEnabled()
186         return if (tracingEnabled) {
187             traceAsync(DEFAULT_TRACK_NAME, tag(), block)
188         } else {
189             block()
190         }
191     }
192 
193     /**
194      * Creates an async slice in the default track.
195      *
196      * The [tag] is computed only if tracing is enabled. See [traceAsync].
197      */
198     @JvmStatic
traceAsyncnull199     public inline fun <T> traceAsync(trackName: String, tag: () -> String, block: () -> T): T {
200         val tracingEnabled = Trace.isEnabled()
201         return if (tracingEnabled) {
202             traceAsync(trackName, tag(), block)
203         } else {
204             block()
205         }
206     }
207 
208     /**
209      * Creates an async slice in a track with [trackName] while [block] runs.
210      *
211      * This can be used to trace coroutine code. [sliceName] will be the name of the slice,
212      * [trackName] of the track. The track is one of the rows visible in a perfetto trace inside the
213      * app process.
214      */
215     @JvmStatic
traceAsyncnull216     public inline fun <T> traceAsync(trackName: String, sliceName: String, block: () -> T): T {
217         contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
218         return traceAsync(Trace.TRACE_TAG_APP, trackName, sliceName, block)
219     }
220 
221     /** Creates an async slice in a track with [trackName] while [block] runs. */
222     @JvmStatic
traceAsyncnull223     public inline fun <T> traceAsync(
224         traceTag: Long,
225         trackName: String,
226         sliceName: String,
227         block: () -> T,
228     ): T {
229         contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
230         val cookie = ThreadLocalRandom.current().nextInt()
231         Trace.asyncTraceForTrackBegin(traceTag, trackName, sliceName, cookie)
232         try {
233             return block()
234         } finally {
235             Trace.asyncTraceForTrackEnd(traceTag, trackName, cookie)
236         }
237     }
238 
239     /** Creates an async slice in a track with [trackName] while [block] runs. */
240     @JvmStatic
traceAsyncnull241     public inline fun <T> traceAsync(
242         traceTag: Long,
243         trackName: String,
244         sliceName: () -> String,
245         block: () -> T,
246     ): T {
247         contract {
248             callsInPlace(sliceName, InvocationKind.AT_MOST_ONCE)
249             callsInPlace(block, InvocationKind.EXACTLY_ONCE)
250         }
251         val tracingEnabled = Trace.isEnabled()
252         return if (tracingEnabled) {
253             return traceAsync(traceTag, trackName, sliceName(), block)
254         } else {
255             block()
256         }
257     }
258 
259     /** Starts an async slice, and returns a runnable that stops the slice. */
260     @JvmStatic
traceAsyncClosablenull261     public fun traceAsyncClosable(
262         traceTag: Long = Trace.TRACE_TAG_APP,
263         trackName: String,
264         sliceName: String,
265     ): () -> Unit {
266         val cookie = ThreadLocalRandom.current().nextInt()
267         Trace.asyncTraceForTrackBegin(traceTag, trackName, sliceName, cookie)
268         return { Trace.asyncTraceForTrackEnd(traceTag, trackName, cookie) }
269     }
270 
271     /** Starts an async slice, and returns a runnable that stops the slice. */
272     @JvmStatic
273     @JvmOverloads
traceAsyncClosablenull274     public fun traceAsyncClosable(
275         traceTag: Long = Trace.TRACE_TAG_APP,
276         trackGroupName: String,
277         trackName: String,
278         sliceName: String,
279     ): () -> Unit {
280         val groupedTrackName = trackGroup(trackGroupName, trackName)
281         val cookie = ThreadLocalRandom.current().nextInt()
282         Trace.asyncTraceForTrackBegin(traceTag, groupedTrackName, sliceName, cookie)
283         return { Trace.asyncTraceForTrackEnd(traceTag, groupedTrackName, cookie) }
284     }
285 }
286