App-driven profiling

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:

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.1")
implementation("androidx.core:core:1.19.0")
}

Groovy

dependencies{
implementation'androidx.tracing:tracing:2.0.1'
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
funProfiledScreen(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.
valappContext=LocalContext.current.applicationContext
valscope=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){
valcallbackExecutor=Dispatchers.IO.asExecutor()
valresultCallback=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}"
)
}
}
valstopSignal=CancellationSignal()
valrequestBuilder=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.
privateconstvalSTARTUP_PADDING_MS=100L
funheavyOperation(){
// Background computations to profile.
}

Java

voidheavyOperation(){
// Computations you want to profile
}
voidsampleRecordSystemTrace(){
ExecutormainExecutor=Executors.newSingleThreadExecutor();
Consumer<ProfilingResult>resultCallback=
newConsumer<ProfilingResult>(){
@Override
publicvoidaccept(ProfilingResultprofilingResult){
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());
}
}
};
CancellationSignalstopSignal=newCancellationSignal();
SystemTraceRequestBuilderrequestBuilder=newSystemTraceRequestBuilder();
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:

  1. Set up the executor. Create an Executor to 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.

  2. Handle profiling results. Create a Consumer<ProfilingResult> object. The system uses this object to send profiling results from ProfilingManager back to your app.

  3. Build the profiling request. Create a SystemTraceRequestBuilder to set up your profiling session. This builder lets you customize ProfilingManager trace 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, 60000 sets a 60-second trace. The trace automatically ends after the specified duration if CancellationSignal isn't triggered before that.
    • Choose a buffer policy. Use setBufferFillPolicy() to define how trace data is stored. BufferFillPolicy.RING_BUFFER means 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.
  4. 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.

  5. Start and receive results. When you call requestProfiling(), ProfilingManager starts a profiling session in the background. Once profiling is done, it sends the ProfilingResult to your resultCallback#accept method. If profiling finishes successfully, the ProfilingResult provides the path where the trace was saved on your device through ProfilingResult#getResultFilePath. You can get this file programmatically or, for local profiling, by running adb pull <trace_path> from your computer.

  6. 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.

Content and code samples on this page are subject to the licenses described in the Content License. Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.

Last updated 2026年08月26日 UTC.