Beyond the Surface: Mastering "In-process Tracing" with Android Tracing 2.0

 Unlocking the "Execution Story" of Coroutines and Async Work with Tracing 2.0.

In-process Tracing

Modern Android development is no longer linear. With Kotlin Coroutines, thread pools, and background workers firing simultaneously, apps have become highly concurrent ecosystems.

When things slow down, traditional tools tell us that the app is lagging, but they rarely tell us why a specific sequence of events led to that lag. Enter In-process Tracing (Tracing 2.0) — a shift from simple resource profiling to high-fidelity execution storytelling.

🧐 What is In-process Tracing?

While standard system tracing (like ATrace) captures high-level kernel activity and CPU scheduling, In-process Tracing (via androidx.tracing:tracing:2.0.0-alpha01) allows you to instrument your code from the inside out.

Think of it this way:

  • System Tracing: A satellite view of city traffic. You see the congestion, but not the cause.
  • In-process Tracing: A GPS log for every delivery vehicle. You see exactly when a package was picked up, which route was taken, and where the delay occurred.

It fills the gap between “The CPU is busy” and “The user’s login flow is stalled because of a specific database lock.”

🛠️ Implementation: Tracking Work Across Hops

The power of Tracing 2.0 lies in its ability to track logical “slices” of work, even if they move across different threads. However, unlike standard synchronous tracing, async tracing requires intentional management.

import androidx.tracing.trace // Jetpack extension helper for sync blocks
import androidx.tracing.Trace // Platform-level API for async sections

class UserProfileRepository(private val api: ApiService, private val db: UserDatabase) {

suspend fun loadFullUserProfile(userId: String) {
// 'trace {}' is a scoped helper for synchronous blocks
trace("LoadUserProfile:$userId") {
val profile = fetchFromNetwork(userId)
saveToLocalCache(profile)
}
}

private suspend fun fetchFromNetwork(userId: String): Profile {
val traceName = "NetworkFetch"
// Ensure cookies are unique for overlapping async operations
// to avoid trace collisions in Perfetto.
val cookie = userId.hashCode()

Trace.beginAsyncSection(traceName, cookie)
return try {
api.getUser(userId)
} finally {
// Must manually end the section. This ensures the slice
// spans correctly even if the coroutine resumes on another thread.
Trace.endAsyncSection(traceName, cookie)
}
}

private fun saveToLocalCache(data: Profile) {
// Record numeric data (like payload sizes) using counters
// for visual graphing in the profiler.
Trace.setCounter("LastResponseSize", data.size.toLong())

trace("DiskWrite") {
db.userDao().insert(data)
}
}
}

Mental Model Tip: When analyzing traces, think in terms of logical flows (user actions) rather than threads. Threads are an implementation detail; traces let you reconstruct the user’s intent.

⚠️ Common Pitfalls to Avoid

Even with the best intentions, it’s easy to muddy your performance data. Watch out for these:

  • Forgetting to end async sections: This leads to traces that appear “stuck” and makes your timelines misleading. Always use try-finally.
  • Reusing cookies for parallel work: If two concurrent network calls use the same cookie, they will collide and overwrite each other in the visualization.
  • Over-instrumentation: Tracing every tiny helper function adds noise and overhead. Focus on the high-level “story.”
  • Assuming Coroutine Awareness: Tracing spans threads, but it does not automatically track coroutine scopes. Instrumentation across suspension points is your responsibility.

🚀 Why Traditional Profiling Isn’t Enough

Press enter or click to view image in full size
Why Traditional Profiling Isn’t Enough
Why Traditional Profiling Isn’t Enough

⏱️ When (and When Not) to Use It

  • Debugging “Ghost” Stutters: When the app feels slow but the CPU usage looks fine.
  • Building SDKs: Help your users understand how your library impacts their main thread.
  • Complex Async Chains: Visualizing waterfalls of network calls and UI updates.
  • Pure CPU hot paths: Use a CPU profiler for raw math/logic optimizations.
  • One-off crashes: Use Logcat or crash reporting tools.
  • Extremely hot inner loops: The tracing overhead may distort your results.

🧠 The Missing Lens for Concurrency

The beauty of Tracing 2.0 isn’t just in the data — it’s in the visualization. While the Android Studio Profiler is great for quick checks, exporting your traces to ui.perfetto.dev is where the “execution story” truly comes to life. You can see the relationship between threads and pinpoint exactly where a coroutine was waiting for a dispatcher.

🙋‍♂️ Frequently Asked Questions (FAQs)

Is Tracing 2.0 ready for production?

It is currently in alpha. It is best used for internal profiling and performance regression testing rather than shipping in production code unless strictly necessary.

Does it automatically follow Coroutines?

No. You must manually instrument your suspension boundaries using the async APIs. It makes the data compatible with async flows, but you provide the instruction.

How do I view these traces?

Record a trace using the System Tracing app on your device or via adb, and then load the resulting .perfetto-trace file into the Perfetto web UI.

💬 Let’s Discuss!

  • Have you ever found a bug in a trace that was invisible in your logs?
  • What is the most complex async flow in your app right now?
  • How are you currently measuring the “resumption delay” of your coroutines?

📘 Master Your Next Technical Interview

Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend “Mastering Data Structures & Algorithms in Java”. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.

Comments

Popular posts from this blog

No More _state + state: Simplifying ViewModels with Kotlin 2.3

Why You Should Stop Passing ViewModels Around Your Compose UI Tree 🚫

Is Jetpack Compose Making Your APK Fatter? (And How to Fix It)