• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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 @file:OptIn(ExperimentalContracts::class)
18 
19 package com.android.app.tracing.coroutines
20 
21 import android.os.Trace
22 import com.android.app.tracing.beginSlice
23 import com.android.app.tracing.endSlice
24 import java.util.ArrayDeque
25 import kotlin.contracts.ExperimentalContracts
26 import kotlin.math.max
27 import kotlinx.coroutines.CoroutineStart.UNDISPATCHED
28 
29 /**
30  * Represents a section of code executing in a coroutine. This may be split up into multiple slices
31  * on different threads as the coroutine is suspended and resumed.
32  *
33  * @see traceCoroutine
34  */
35 private typealias TraceSection = String
36 
37 /** Use a final subclass to avoid virtual calls (b/316642146). */
38 @PublishedApi
39 internal class TraceDataThreadLocal : ThreadLocal<TraceStorage?>() {
40     override fun initialValue(): TraceStorage? {
41         return if (com.android.systemui.Flags.coroutineTracing()) {
42             TraceStorage(null)
43         } else {
44             null
45         }
46     }
47 }
48 
49 /**
50  * There should only be one instance of this class per thread.
51  *
52  * @param openSliceCount ThreadLocal counter for how many open trace sections there are on the
53  *   current thread. This is needed because it is possible that on a multi-threaded dispatcher, one
54  *   of the threads could be slow, and [TraceContextElement.restoreThreadContext] might be invoked
55  *   _after_ the coroutine has already resumed and modified [TraceData] - either adding or removing
56  *   trace sections and changing the count. If we did not store this thread-locally, then we would
57  *   incorrectly end too many or too few trace sections.
58  */
59 @PublishedApi
60 internal class TraceStorage(internal var data: TraceData?) {
61 
62     /**
63      * Counter for tracking which index to use in the [continuationIds] and [openSliceCount] arrays.
64      * `contIndex` is used to keep track of the stack used for managing tracing state when
65      * coroutines are resumed and suspended in a nested way.
66      * * `-1` indicates no coroutine is currently running
67      * * `0` indicates one coroutine is running
68      * * `>1` indicates the current coroutine is resumed inside another coroutine, e.g. due to an
69      *   unconfined dispatcher or [UNDISPATCHED] launch.
70      */
71     private var contIndex = -1
72 
73     /**
74      * Count of slices opened on the current thread due to current [TraceData] that must be closed
75      * when it is removed. If another [data] overwrites the current one, all trace sections due to
76      * current [data] must be closed. The overwriting [data] will handle updating itself when
77      * [TraceContextElement.updateThreadContext] is called for it.
78      *
79      * Expected nesting should never exceed 255, so use a [ByteArray]. If nesting _does_ exceed 255,
80      * it indicates there is already something very wrong with the trace, so we will not waste CPU
81      * cycles error checking.
82      */
83     private var openSliceCount = ByteArray(INITIAL_THREAD_LOCAL_STACK_SIZE)
84 
85     private var continuationIds: IntArray? =
86         if (android.os.Flags.perfettoSdkTracingV2()) IntArray(INITIAL_THREAD_LOCAL_STACK_SIZE)
87         else null
88 
89     private val debugCounterTrack: String? =
90         if (DEBUG) "TCE#${Thread.currentThread().threadId()}" else null
91 
92     /**
93      * Adds a new trace section to the current trace data. The slice will be traced on the current
94      * thread immediately. This slice will not propagate to parent coroutines, or to child
95      * coroutines that have already started.
96      */
97     @PublishedApi
beginCoroutineTracenull98     internal fun beginCoroutineTrace(name: String) {
99         val data = data ?: return
100         data.beginSpan(name)
101         if (0 <= contIndex && contIndex < openSliceCount.size) {
102             openSliceCount[contIndex]++
103         }
104     }
105 
106     /**
107      * Ends the trace section and validates it corresponds with an earlier call to
108      * [beginCoroutineTrace]. The trace slice will immediately be removed from the current thread.
109      * This information will not propagate to parent coroutines, or to child coroutines that have
110      * already started.
111      *
112      * @return true if span was ended, `false` if not
113      */
114     @PublishedApi
endCoroutineTracenull115     internal fun endCoroutineTrace() {
116         if (data?.endSpan() == true && 0 <= contIndex && contIndex < openSliceCount.size) {
117             openSliceCount[contIndex]--
118         }
119     }
120 
121     /** Update [data] for continuation */
updateDataForContinuationnull122     fun updateDataForContinuation(contextTraceData: TraceData?, contId: Int) {
123         data = contextTraceData
124         val n = ++contIndex
125         if (DEBUG) Trace.traceCounter(Trace.TRACE_TAG_APP, debugCounterTrack!!, n)
126         if (n < 0 || MAX_THREAD_LOCAL_STACK_SIZE <= n) return // fail-safe
127         var size = openSliceCount.size
128         if (n >= size) {
129             size = max(2 * size, MAX_THREAD_LOCAL_STACK_SIZE)
130             openSliceCount = openSliceCount.copyInto(ByteArray(size))
131             continuationIds = continuationIds?.copyInto(IntArray(size))
132         }
133         openSliceCount[n] = data?.beginAllOnThread() ?: 0
134         if (0 < contId) continuationIds?.set(n, contId)
135     }
136 
137     /** Update [data] for suspension */
restoreDataForSuspensionnull138     fun restoreDataForSuspension(oldState: TraceData?): Int {
139         data = oldState
140         val n = contIndex--
141         if (DEBUG) Trace.traceCounter(Trace.TRACE_TAG_APP, debugCounterTrack!!, n)
142         if (n < 0 || openSliceCount.size <= n) return 0 // fail-safe
143         if (Trace.isTagEnabled(Trace.TRACE_TAG_APP)) {
144             val lastState = openSliceCount[n]
145             var i = 0
146             while (i < lastState) {
147                 endSlice()
148                 i++
149             }
150         }
151         return continuationIds?.let { if (n < it.size) it[n] else null } ?: 0
152     }
153 }
154 
155 /**
156  * Used for storing trace sections so that they can be added and removed from the currently running
157  * thread when the coroutine is suspended and resumed.
158  *
159  * @property currentId ID of associated TraceContextElement
160  * @property strictMode Whether to add additional checks to the coroutine machinery, throwing a
161  *   `ConcurrentModificationException` if TraceData is modified from the wrong thread. This should
162  *   only be set for testing.
163  * @see traceCoroutine
164  */
165 @PublishedApi
166 internal class TraceData(internal val currentId: Int, private val strictMode: Boolean) {
167 
168     internal lateinit var slices: ArrayDeque<TraceSection>
169 
170     /**
171      * Adds current trace slices back to the current thread. Called when coroutine is resumed.
172      *
173      * @return number of new trace sections started
174      */
beginAllOnThreadnull175     internal fun beginAllOnThread(): Byte {
176         if (Trace.isTagEnabled(Trace.TRACE_TAG_APP)) {
177             strictModeCheck()
178             if (::slices.isInitialized) {
179                 var count: Byte = 0
180                 slices.descendingIterator().forEach { sectionName ->
181                     beginSlice(sectionName)
182                     count++
183                 }
184                 return count
185             }
186         }
187         return 0
188     }
189 
190     /**
191      * Creates a new trace section with a unique ID and adds it to the current trace data. The slice
192      * will also be added to the current thread immediately. This slice will not propagate to parent
193      * coroutines, or to child coroutines that have already started. The unique ID is used to verify
194      * that the [endSpan] is corresponds to a [beginSpan].
195      */
beginSpannull196     internal fun beginSpan(name: String) {
197         strictModeCheck()
198         if (!::slices.isInitialized) {
199             slices = ArrayDeque<TraceSection>(4)
200         }
201         slices.push(name)
202         beginSlice(name)
203     }
204 
205     /**
206      * Ends the trace section and validates it corresponds with an earlier call to [beginSpan]. The
207      * trace slice will immediately be removed from the current thread. This information will not
208      * propagate to parent coroutines, or to child coroutines that have already started.
209      *
210      * @return `true` if [endSlice] was called, `false` otherwise
211      */
endSpannull212     internal fun endSpan(): Boolean {
213         strictModeCheck()
214         // Should never happen, but we should be defensive rather than crash the whole application
215         if (::slices.isInitialized && !slices.isEmpty()) {
216             slices.pop()
217             endSlice()
218             return true
219         } else if (strictMode) {
220             throw IllegalStateException(INVALID_SPAN_END_CALL_ERROR_MESSAGE)
221         }
222         return false
223     }
224 
toStringnull225     public override fun toString(): String =
226         if (DEBUG) {
227             if (::slices.isInitialized) {
228                 "{${slices.joinToString(separator = "\", \"", prefix = "\"", postfix = "\"")}}"
229             } else {
230                 "{<uninitialized>}"
231             } + "@${hashCode()}"
232         } else super.toString()
233 
strictModeChecknull234     private fun strictModeCheck() {
235         if (strictMode && traceThreadLocal.get()?.data !== this) {
236             throw ConcurrentModificationException(STRICT_MODE_ERROR_MESSAGE)
237         }
238     }
239 }
240 
241 private const val INITIAL_THREAD_LOCAL_STACK_SIZE = 4
242 
243 /**
244  * The maximum allowed stack size for coroutine re-entry. Anything above this will cause malformed
245  * traces. It should be set to a high number that should never happen, meaning if it were to occur,
246  * there is likely an underlying bug.
247  */
248 private const val MAX_THREAD_LOCAL_STACK_SIZE = 512
249 
250 private const val INVALID_SPAN_END_CALL_ERROR_MESSAGE =
251     "TraceData#endSpan called when there were no active trace sections in its scope."
252 
253 private const val STRICT_MODE_ERROR_MESSAGE =
254     "TraceData should only be accessed using " +
255         "the ThreadLocal: CURRENT_TRACE.get(). Accessing TraceData by other means, such as " +
256         "through the TraceContextElement's property may lead to concurrent modification."
257