This page shows how to record a system trace using the ProfilingManager API.
ProfilingManager can also record other profile types. This process is similar
to recording a system trace, but each type uses a different builder. The
supported profiles and their builders are:
System Traces: Recorded using
SystemTraceRequestBuilder, which are useful for latency analysis and general performance debugging.Heap dumps: Recorded using
JavaHeapDumpRequestBuilder, which are helpful for memory leak detection and optimization.Heap profiles: Recorded using
HeapProfileRequestBuilder, which are useful for memory optimization.Call stack profiles: Recorded using
StackSamplingRequestBuilder, which are useful for understanding code execution and latency analysis.
Add dependencies
For the best experience with the ProfilingManager API, add the following
Jetpack libraries to your build.gradle.kts file.
Kotlin
dependencies { implementation("androidx.tracing:tracing-ktx:2.0.0") implementation("androidx.core:core:1.19.0") }
Groovy
dependencies { implementation 'androidx.tracing:tracing:2.0.0' implementation 'androidx.core:core:1.19.0' }
Record a system trace
After adding the required dependencies, use the following code to record a system trace. This example shows how to start a profiling session from a composable while safely managing heavy operations off the main thread.
Kotlin
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable
fun ProfiledScreen(modifier: Modifier = Modifier) {
// Use the application context: requestProfiling resolves the ProfilingManager
// system service from it, so there's no reason to hand it a short-lived Activity.
val appContext = LocalContext.current.applicationContext
val scope = rememberCoroutineScope()
Button(
onClick = {
// Run the orchestration off the main thread. Profiling a heavy operation
// on the UI thread would freeze the UI (ANR) and distort the very metrics
// you're trying to capture.
//
// Note: this scope is tied to composition. If the user leaves this screen
// mid-session, the coroutine is cancelled and stopSignal.cancel() might not
// run, but setDurationMs() acts as a safety net and ends the trace.
scope.launch(Dispatchers.Default) {
val callbackExecutor = Dispatchers.IO.asExecutor()
val resultCallback = Consumer<ProfilingResult> { profilingResult ->
if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
Log.d("ProfileTest", "Result file: ${profilingResult.resultFilePath}")
} else {
// errorMessage explains the failure (e.g., rate limiting); keep it.
Log.e(
"ProfileTest",
"Profiling failed errorCode=${profilingResult.errorCode} " +
"errorMessage=${profilingResult.errorMessage}"
)
}
}
val stopSignal = CancellationSignal()
val requestBuilder = SystemTraceRequestBuilder().apply {
setCancellationSignal(stopSignal)
setTag("FOO") // Caller-supplied tag for identification.
setDurationMs(60000) // Hard cap: ends the session if cancel() never fires.
setBufferFillPolicy(BufferFillPolicy.RING_BUFFER)
setBufferSizeKb(32768)
}
// 1. Start the session. This is asynchronous system IPC. The tracing
// engine takes a moment to start and allocate buffers.
requestProfiling(appContext, requestBuilder.build(), callbackExecutor, resultCallback)
// 2. The API exposes no "profiling started" signal, so pad with a short,
// best-effort delay before running the code you care about. This is
// approximate. Increase it on slower or heavily loaded devices.
delay(STARTUP_PADDING_MS)
// 3. The session is already recording every thread in your app. This slice
// doesn't scope what's captured. It just labels this region of the
// timeline so heavyOperation() is easier to find. trace { } closes the
// section even if the block throws.
trace("MyApp:HeavyOperation") {
heavyOperation()
}
// 4. Stop recording. Until this fires or the setDurationMs() cap is
// reached (whichever comes first), the session keeps capturing app-wide
// activity.
stopSignal.cancel()
}
}
) {
Text("Run & Profile Heavy Operation")
}
}
// Best-effort wait for the system trace engine to initialize before profiling.
// There is no deterministic start callback; tune this for your target devices.
private const val STARTUP_PADDING_MS = 100L
fun heavyOperation() {
// Background computations to profile.
}
Java
void heavyOperation() {
// Computations you want to profile
}
void sampleRecordSystemTrace() {
Executor mainExecutor = Executors.newSingleThreadExecutor();
Consumer<ProfilingResult> resultCallback =
new Consumer<ProfilingResult>() {
@Override
public void accept(ProfilingResult profilingResult) {
if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE) {
Log.d(
"ProfileTest",
"Received profiling result file=" + profilingResult.getResultFilePath());
setupProfileUploadWorker(profilingResult.getResultFilePath());
} else {
Log.e(
"ProfileTest",
"Profiling failed errorcode="
+ profilingResult.getErrorCode()
+ " errormsg="
+ profilingResult.getErrorMessage());
}
}
};
CancellationSignal stopSignal = new CancellationSignal();
SystemTraceRequestBuilder requestBuilder = new SystemTraceRequestBuilder();
requestBuilder.setCancellationSignal(stopSignal);
requestBuilder.setTag("FOO");
requestBuilder.setDurationMs(60000);
requestBuilder.setBufferFillPolicy(BufferFillPolicy.RING_BUFFER);
requestBuilder.setBufferSizeKb(32768);
Profiling.requestProfiling(getApplicationContext(), requestBuilder.build(), mainExecutor,
resultCallback);
// Wait some time for profiling to start.
Trace.beginSection("MyApp:HeavyOperation");
heavyOperation();
Trace.endSection();
// Once the interesting code section is profiled, stop profile
stopSignal.cancel();
}
The sample code sets up and manages the profiling session by going through the following steps:
Set up the executor. Create an
Executorto define the thread that will receive the profiling results. Profiling happens in the background. Using a non-UI thread executor helps prevent Application Not Responding (ANR) errors if you add more processing to the callback later.Handle profiling results. Create a
Consumer<ProfilingResult>object. The system uses this object to send profiling results fromProfilingManagerback to your app.Build the profiling request. Create a
SystemTraceRequestBuilderto set up your profiling session. This builder lets you customizeProfilingManagertrace settings. Customizing the builder is optional; if you don't, the system uses default settings.- Define a tag. Use
setTag()to add a tag to the trace name. This tag helps you identify the trace. - Optional: Set the duration. Use
setDurationMs()to specify how long to profile in milliseconds. For example,60000sets a 60-second trace. The trace automatically ends after the specified duration ifCancellationSignalisn't triggered before that. - Choose a buffer policy. Use
setBufferFillPolicy()to define how trace data is stored.BufferFillPolicy.RING_BUFFERmeans that when the buffer is full, new data overwrites the oldest data, keeping a continuous record of recent activity. - Set a buffer size. Use
setBufferSizeKb()to specify a buffer size for tracing which you can use to control the size of the output trace file.
- Define a tag. Use
Optional: Manage the session lifecycle. Create a
CancellationSignal. This object lets you stop the profiling session whenever you want, giving you precise control over its length.Start and receive results. When you call
requestProfiling(),ProfilingManagerstarts a profiling session in the background. Once profiling is done, it sends theProfilingResultto yourresultCallback#acceptmethod. If profiling finishes successfully, theProfilingResultprovides the path where the trace was saved on your device throughProfilingResult#getResultFilePath. You can get this file programmatically or, for local profiling, by runningadb pull <trace_path>from your computer.Add custom trace points. You can add custom trace points in your app's code. In the previous code example, the
trace("MyApp:HeavyOperation") { ... }block creates a custom slice in the generated profile.