Instrumenting Android apps/platform with atrace

In this guide, you'll learn how to:

This page is mainly intended for:

Atrace slices example

Atrace is an API introduced in Android 4.3 that predates that allows you to add instrumentation to your code. It is still supported and in use, and interoperates well with Perfetto.

Under the hoods, Atrace forwards events up to the kernel ftrace ring-buffer and gets fetched together with the rest of scheduling data and other system-level trace data. Atrace is both:

  1. A public API, exposed both to Java/Kt code via the Android SDK and C/C++ code via the NDK, that developers can use to enrich traces annotating their apps.
  2. A private platform API used to annotate several framework functions and the implementation of core system services. It provides developers with insights about what the framework is doing under the hoods.

The main difference between the two is that the private platform API allows specifying a tag (also known as category), while the SDK/NDK interface implicitly uses TRACE_TAG_APP.

In both cases, Atrace allows you to manually add instrumentation around code wall timing and numeric values, e.g. to annotate the beginning or end of functions, logical user journeys, state changes.

Thread-scoped synchronous slices

Slices are used to create rectangles around the execution of code and visually form a pseudo-callstack.

Semantic and constraints:

Refer to frameworks/base/core/java/android/os/Trace.java

import android.os.Trace; import static android.os.Trace.TRACE_TAG_AUDIO; public void playSound(String path) { Trace.traceBegin(TRACE_TAG_AUDIO, "PlaySound"); try { // Measure the time it takes to open the sound sevice. Trace.traceBegin(TRACE_TAG_AUDIO, "OpenAudioDevice"); try { SoundDevice dev = openAudioDevice(); } finally { Trace.traceEnd(); } for(...) { Trace.traceBegin(TRACE_TAG_AUDIO, "SendBuffer"); try { sendAudioBuffer(dev, ...) } finally { Trace.traceEnd(); } // Log buffer usage statistics in the trace. Trace.setCounter(TRACE_TAG_AUDIO, "SndBufferUsage", dev->buffer) ... } } finally { Trace.traceEnd(); // End of the root PlaySound slice } }

// ATRACE_TAG is the category that will be used in this translation unit. // Pick one of the categories defined in Android's // system/core/libcutils/include/cutils/trace.h #define ATRACE_TAG ATRACE_TAG_AUDIO #include <cutils/trace.h> void PlaySound(const char* path) { ATRACE_BEGIN("PlaySound"); // Measure the time it takes to open the sound sevice. ATRACE_BEGIN("OpenAudioDevice"); struct snd_dev* dev = OpenAudioDevice(); ATRACE_END(); for(...) { ATRACE_BEGIN("SendBuffer"); SendAudioBuffer(dev, ...) ATRACE_END(); // Log buffer usage statistics in the trace. ATRACE_INT("SndBufferUsage", dev->buffer); ... } ATRACE_END(); // End of the root PlaySound slice }

Refer to the SDK reference documentation for os.trace.

// You cannot choose a tag/category when using the SDK API. // Implicitly all calls use the ATRACE_TAG_APP tag. import android.os.Trace; public void playSound(String path) { try { ATrace_beginSection("PlaySound"); // Measure the time it takes to open the sound sevice. Trace.beginSection("OpenAudioDevice"); try { SoundDevice dev = openAudioDevice(); } finally { Trace.endSection(); } for(...) { Trace.beginSection("SendBuffer"); try { sendAudioBuffer(dev, ...) } finally { Trace.endSection(); } // Log buffer usage statistics in the trace. Trace.setCounter("SndBufferUsage", dev->buffer) ... } } finally { Trace.endSection(); // End of the root PlaySound slice } }

Refer to the NDK reference documentation for Tracing.

// You cannot choose a tag/category when using the NDK API. // Implicitly all calls use the ATRACE_TAG_APP tag. #include <android/trace.h> void PlaySound(const char* path) { ATrace_beginSection("PlaySound"); // Measure the time it takes to open the sound sevice. ATRACE_BEGIN("OpenAudioDevice"); struct snd_dev* dev = OpenAudioDevice(); ATrace_endSection(); for(...) { ATrace_beginSection("SendBuffer"); SendAudioBuffer(dev, ...) ATrace_endSection(); // Log buffer usage statistics in the trace. ATrace_setCounter("SndBufferUsage", dev->buffer) ... } ATrace_endSection(); // End of the root PlaySound slice }

Counters

Semantic and constraints:

Refer to frameworks/base/core/java/android/os/Trace.java

import android.os.Trace; import static android.os.Trace.TRACE_TAG_AUDIO; public void playSound(String path) { SoundDevice dev = openAudioDevice(); for(...) { sendAudioBuffer(dev, ...) ... // Log buffer usage statistics in the trace. Trace.setCounter(TRACE_TAG_AUDIO, "SndBufferUsage", dev->buffer.used_bytes) } }

// ATRACE_TAG is the category that will be used in this translation unit. // Pick one of the categories defined in Android's // system/core/libcutils/include/cutils/trace.h #define ATRACE_TAG ATRACE_TAG_AUDIO #include <cutils/trace.h> void PlaySound(const char* path) { struct snd_dev* dev = OpenAudioDevice(); for(...) { SendAudioBuffer(dev, ...) // Log buffer usage statistics in the trace. ATRACE_INT("SndBufferUsage", dev->buffer.used_bytes); } }

Refer to the SDK reference documentation for os.trace.

// You cannot choose a tag/category when using the SDK API. // Implicitly all calls use the ATRACE_TAG_APP tag. import android.os.Trace; public void playSound(String path) { SoundDevice dev = openAudioDevice(); for(...) { sendAudioBuffer(dev, ...) // Log buffer usage statistics in the trace. Trace.setCounter("SndBufferUsage", dev->buffer.used_bytes) } }

Refer to the NDK reference documentation for Tracing.

// You cannot choose a tag/category when using the NDK API. // Implicitly all calls use the ATRACE_TAG_APP tag. #include <android/trace.h> void PlaySound(const char* path) { struct snd_dev* dev = OpenAudioDevice(); for(...) { SendAudioBuffer(dev, ...) // Log buffer usage statistics in the trace. ATrace_setCounter("SndBufferUsage", dev->buffer.used_bytes) } }

Cross-thread async slices

Async slices allow to trace logical operations that might begin and end on different threads. They are the same concept of track events in the Perfetto SDK.

Because begin/end can happen on different thread, you need to pass a cookie to each begin/end function. The cookie is just an integer number used to match begin/end pairs. The cookie is usually derived from a pointer or a unique ID that represents the logical operation being traced (e.g. a job id).

Semantic and constraints:

Refer to frameworks/base/core/java/android/os/Trace.java

import android.os.Trace; import static android.os.Trace.TRACE_TAG_NETWORK; public class AudioRecordActivity extends Activity { private AtomicInteger lastJobId = new AtomicInteger(0); private static final String TRACK_NAME = "User Journeys"; ... button.setOnClickListener(v -> { int jobId = lastJobId.incrementAndGet(); Trace.asyncTraceForTrackBegin(TRACE_TAG_NETWORK, TRACK_NAME, "Load profile", jobId); // Simulate async work (e.g., a network request) new Thread(() -> { Thread.sleep(800); // emulate latency Trace.asyncTraceForTrackEnd(TRACE_TAG_NETWORK, TRACK_NAME, jobId); }).start(); }); ... }

// ATRACE_TAG is the category that will be used in this translation unit. // Pick one of the categories defined in Android's // system/core/libcutils/include/cutils/trace.h #define ATRACE_TAG ATRACE_TAG_NETWORK #include <cutils/trace.h> #include <thread> #include <chrono> #include <atomic> static constexpr const char* kTrackName = "User Journeys"; void onButtonClicked() { static std::atomic<int> lastJobId{0}; int jobId = ++lastJobId; ATRACE_ASYNC_FOR_TRACK_BEGIN(kTrackName, "Load profile", jobId); std::thread([jobId]() { std::this_thread::sleep_for(std::chrono::milliseconds(800)); ATRACE_ASYNC_FOR_TRACK_END(kTrackName, jobId); }).detach(); }

Refer to the SDK reference documentation for os.trace.

// You cannot choose a tag/category when using the SDK API. // Implicitly all calls use the ATRACE_TAG_APP tag. import android.os.Trace; public class AudioRecordActivity extends Activity { private AtomicInteger lastJobId = new AtomicInteger(0); ... button.setOnClickListener(v -> { int jobId = lastJobId.incrementAndGet(); Trace.beginAsyncSection("Load profile", jobId); // Simulate async work (e.g., a network request) new Thread(() -> { Thread.sleep(800); // emulate latency Trace.endAsyncSection("Load profile", jobId); }).start(); }); ... }

Refer to the NDK reference documentation for Tracing.

// You cannot choose a tag/category when using the NDK API. // Implicitly all calls use the ATRACE_TAG_APP tag. #include <android/trace.h> #include <thread> #include <chrono> #include <atomic> void onButtonClicked() { static std::atomic<int> lastJobId{0}; int jobId = ++lastJobId; ATrace_beginAsyncSection("Load profile", jobId); std::thread([jobId]() { std::this_thread::sleep_for(std::chrono::milliseconds(800)); ATrace_endAsyncSection("Load profile", jobId); }).detach(); }

Should I use Atrace or the Perfetto Tracing SDK?

At the time of writing, there isn't a clear-cut answer to this question. Our team is working on providing a replacement SDK that can subsume all the atrace use cases, but we are not there yet. So the answer is: depends.

When to prefer Atrace When to prefer the Tracing SDK
You need something simple that just works. You need more advanced features (e.g. flows).
You are okay with one on/off toggle for the whole app. (If you are in the Android system you can only se a limited set of tags) You need fine-grained control over tracing categories.
You are okay with events being multiplexed in the main ftace buffer. You want control over muxing vents in different buffers.
Instrumentation overhead is not a big concern, your trace points are hit sporadically. You want ininmal overhead for your instrumentation points. Your trace points are frequent (every 10ms or less)

If you are an unbundled app

You should consider using androidx.tracing from Jetpack. We work closely with the Jetpack project. Using androidx.tracing is going to lead to a smoother migration path once we improve our SDK.

Recording the trace

In order to record atrace you must enable the linux.ftrace data source and add in the ftrace_config:

You can see the full list of atrace categories here.

Atrace recording via UI

curl -O https://raw.githubusercontent.com/google/perfetto/main/tools/record_android_trace python3 record_android_trace \ -o trace_file.perfetto-trace \ -t 10s \ # To record atrace from apps. -a 'com.myapp' \ # or '*' for tracing all apps # To record atrace from system services. am wm webview

data_sources { config { name: "linux.ftrace" ftrace_config { atrace_categories: "am" atrace_categories: "wm" atrace_categories: "webview" atrace_apps: "com.myapp1" atrace_apps: "com.myapp2" } } }

Next Steps

Now that you've learned how to instrument your code with ATrace, here are some other documents you might find useful:

Recording traces

Other Android data sources

Analyzing traces