/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.app.tracing import android.annotation.SuppressLint import android.os.Trace import com.android.app.tracing.coroutines.traceCoroutine import java.util.concurrent.ThreadLocalRandom /** * Writes a trace message to indicate that a given section of code has begun running __on the * current thread__. This must be followed by a corresponding call to [endSlice] in a reasonably * short amount of time __on the same thread__ (i.e. _before_ the thread becomes idle again and * starts running other, unrelated work). * * Calls to [beginSlice] and [endSlice] may be nested, and they will render in Perfetto as follows: * ``` * Thread #1 | [==========================] * | [==============] * | [====] * ``` * * This function is provided for convenience to wrap a call to [Trace.traceBegin], which is more * verbose to call than [Trace.beginSection], but has the added benefit of not throwing an * [IllegalArgumentException] if the provided string is longer than 127 characters. We use the term * "slice" instead of "section" to be consistent with Perfetto. * * # Avoiding malformed traces * * Improper usage of this API will lead to malformed traces with long slices that sometimes never * end. This will look like the following: * ``` * Thread #1 | [===================================================================== ... * | [==============] [====================================== ... * | [=======] [======] [===================== ... * | [=======] * ``` * * To avoid this, [beginSlice] and [endSlice] should never be called from `suspend` blocks (instead, * use [traceCoroutine] for tracing suspending functions). While it would be technically okay to * call from a suspending function if that function were to only wrap non-suspending blocks with * [beginSlice] and [endSlice], doing so is risky because suspend calls could be mistakenly added to * that block as the code is refactored. * * Additionally, it is _not_ okay to call [beginSlice] when registering a callback and match it with * a call to [endSlice] inside that callback, even if the callback runs on the same thread. Doing so * would cause malformed traces because the [beginSlice] wasn't closed before the thread became idle * and started running unrelated work. * * @param sliceName The name of the code section to appear in the trace * @see endSlice * @see traceCoroutine */ @SuppressLint("UnclosedTrace") @PublishedApi internal fun beginSlice(sliceName: String) { Trace.traceBegin(Trace.TRACE_TAG_APP, sliceName) } /** * Writes a trace message to indicate that a given section of code has ended. This call must be * preceded by a corresponding call to [beginSlice]. See [beginSlice] for important information * regarding usage. * * @see beginSlice * @see traceCoroutine */ @PublishedApi internal fun endSlice() { Trace.traceEnd(Trace.TRACE_TAG_APP) } /** * Run a block within a [Trace] section. Calls [Trace.beginSection] before and [Trace.endSection] * after the passed block. */ public inline fun traceSection(tag: String, block: () -> T): T { val tracingEnabled = Trace.isEnabled() if (tracingEnabled) beginSlice(tag) return try { // Note that as this is inline, the block section would be duplicated if it is called // several times. For this reason, we're using the try/finally even if tracing is disabled. block() } finally { if (tracingEnabled) endSlice() } } /** * Same as [traceSection], but the tag is provided as a lambda to help avoiding creating expensive * strings when not needed. */ public inline fun traceSection(tag: () -> String, block: () -> T): T { val tracingEnabled = Trace.isEnabled() if (tracingEnabled) beginSlice(tag()) return try { block() } finally { if (tracingEnabled) endSlice() } } public object TraceUtils { public const val TAG: String = "TraceUtils" public const val DEFAULT_TRACK_NAME: String = "AsyncTraces" @JvmStatic public inline fun trace(tag: () -> String, block: () -> T): T { return traceSection(tag) { block() } } @JvmStatic public inline fun trace(tag: String, crossinline block: () -> T): T { return traceSection(tag) { block() } } @JvmStatic public inline fun traceRunnable(tag: String, crossinline block: () -> Unit): Runnable { return Runnable { traceSection(tag) { block() } } } @JvmStatic public inline fun traceRunnable( crossinline tag: () -> String, crossinline block: () -> Unit, ): Runnable { return Runnable { traceSection(tag) { block() } } } /** * Creates an async slice in a track called "AsyncTraces". * * This can be used to trace coroutine code. Note that all usages of this method will appear * under a single track. */ @JvmStatic public inline fun traceAsync(method: String, block: () -> T): T = traceAsync(DEFAULT_TRACK_NAME, method, block) /** Creates an async slice in the default track. */ @JvmStatic public inline fun traceAsync(tag: () -> String, block: () -> T): T { val tracingEnabled = Trace.isEnabled() return if (tracingEnabled) { traceAsync(DEFAULT_TRACK_NAME, tag(), block) } else { block() } } /** * Creates an async slice in the default track. * * The [tag] is computed only if tracing is enabled. See [traceAsync]. */ @JvmStatic public inline fun traceAsync(trackName: String, tag: () -> String, block: () -> T): T { val tracingEnabled = Trace.isEnabled() return if (tracingEnabled) { traceAsync(trackName, tag(), block) } else { block() } } /** * Creates an async slice in a track with [trackName] while [block] runs. * * This can be used to trace coroutine code. [method] will be the name of the slice, [trackName] * of the track. The track is one of the rows visible in a perfetto trace inside the app * process. */ @JvmStatic public inline fun traceAsync(trackName: String, method: String, block: () -> T): T { val cookie = ThreadLocalRandom.current().nextInt() Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, trackName, method, cookie) try { return block() } finally { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, trackName, cookie) } } }