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