Multiplatform ready Tracing Driver
* Support emit perfetto traces on Android and JVM.
* `0` allocations when emitting a trace section.
* Use `mutableTypes` in Wire, and expose platform specific storage sinks.
* Coroutine based context propagation.
* Make it trivial to support other platforms (like Darwin).
1.5 ns 0 allocs TracingOverheadBenchmarkTest.reference
779 ns 0 allocs TracingOverheadBenchmarkTest.customTracingNoSink
3,523 ns 0 allocs TracingOverheadBenchmarkTest.platformTracing
11.0 ns 0 allocs TracingOverheadBenchmarkTest.customTracingDisabled
5,510 ns 2 allocs TracingOverheadBenchmarkTest.customTracingInMemorySink
There are five benchmarks.
* `reference` emits no trace sections (even when tracing is turned on.). This is just purely work.
* `platformTracing` = reference + emit trace sections using the `androidx.tracing` API.
* `customTracingNoSink` = reference + emit trace sections.
The trace packets go through their full lifecycle (including recycling).
The only thing we don't do is to encode the packets into the proto stream.
* `customTracingDisabled` emits no trace sections given tracing is disabled.
* `customTracingInMemorySink` = reference + actually encodes the packets into an in-memory proto stream.
Other Interesting observations:
* Note: Trace packet serialization costs are not fully accounted for in the `platformTracing` benchmark.
The actual serialization happens outside the perview of the benchmark.
* `NoOpSink` is unsurprisingly faster than the platform version (that emits trace sections).
* Tweaking the `bufferSize` on the Okio `Sink` does not make any difference to the benchmark.
Test: Added some JVM tests and tracing overhead benchmarks.
Relnote: Initial commit for Tracing Driver (experimental)
Change-Id: Iaeec406315fbc493339f8b2c1aabdd65c3875919
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index f41573f..972785ea 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -330,6 +330,7 @@
docs(project(":test:uiautomator:uiautomator"))
// androidx.textclassifier is not hosted in androidx
kmpDocs(project(":tracing:tracing"))
+ kmpDocs(project(":tracing:tracing-driver"))
docs(project(":tracing:tracing-ktx"))
docs(project(":tracing:tracing-perfetto"))
docs(project(":tracing:tracing-perfetto-handshake"))
diff --git a/settings.gradle b/settings.gradle
index 3ea7e4a..0145067 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1025,6 +1025,7 @@
includeProject(":test:uiautomator:integration-tests:testapp", [BuildType.MAIN])
includeProject(":tracing:tracing")
includeProject(":tracing:tracing-ktx")
+includeProject(":tracing:tracing-driver")
includeProject(":tracing:tracing-perfetto")
includeProject(":tracing:tracing-perfetto-binary")
includeProject(":tracing:tracing-perfetto-handshake")
diff --git a/tracing/tracing-driver/api/1.3.0-beta01.txt b/tracing/tracing-driver/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..96b7959
--- /dev/null
+++ b/tracing/tracing-driver/api/1.3.0-beta01.txt
@@ -0,0 +1,97 @@
+// Signature format: 4.0
+package androidx.tracing.driver {
+
+ public final class AndroidTraceSink extends androidx.tracing.driver.TraceSink {
+ ctor public AndroidTraceSink(android.content.Context context);
+ ctor public AndroidTraceSink(android.content.Context context, java.io.File traceFile);
+ ctor public AndroidTraceSink(android.content.Context context, okio.BufferedSink bufferedSink, optional kotlin.coroutines.CoroutineContext coroutineContext);
+ method public void close();
+ method public void emit(androidx.tracing.driver.PooledTracePacketArray packetArray);
+ method public void flush();
+ }
+
+ public final class ClockSource_androidKt {
+ method public static inline long nanoTime();
+ }
+
+ public class CounterTrack extends androidx.tracing.driver.Track {
+ ctor public CounterTrack(String name, androidx.tracing.driver.Track parent, optional boolean hasPreamble);
+ method public final void emitDoubleCounterPacket(double value);
+ method public final void emitLongCounterPacket(long value);
+ method public androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ }
+
+ public abstract class EventTrack extends androidx.tracing.driver.Track {
+ ctor public EventTrack(androidx.tracing.driver.TraceContext context, boolean hasPreamble, long uuid, androidx.tracing.driver.Track? parent);
+ method public final androidx.tracing.driver.PooledTracePacket? beginPacket(String name, optional java.util.List<java.lang.Long> flowIds);
+ method public final void emitInstantPacket();
+ method public final androidx.tracing.driver.PooledTracePacket? endPacket(String name);
+ method public final inline <T> T trace(String name, kotlin.jvm.functions.Function0<? extends T> block);
+ method public final suspend <T> Object? traceFlow(String name, optional long flowId, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super T>);
+ }
+
+ public abstract class Poolable<T extends androidx.tracing.driver.Poolable<T>> {
+ method public abstract void recycle();
+ }
+
+ public final class PooledTracePacket extends androidx.tracing.driver.Poolable<androidx.tracing.driver.PooledTracePacket> {
+ method public void encodeTracePacket(com.squareup.wire.ProtoWriter writer);
+ method public void recycle();
+ }
+
+ public final class PooledTracePacketArray extends androidx.tracing.driver.Poolable<androidx.tracing.driver.PooledTracePacketArray> {
+ method public androidx.tracing.driver.PooledTracePacket?[] getPooledTracePacketArray();
+ method public void recycle();
+ property public final androidx.tracing.driver.PooledTracePacket?[] pooledTracePacketArray;
+ }
+
+ public class ProcessTrack extends androidx.tracing.driver.EventTrack {
+ ctor public ProcessTrack(androidx.tracing.driver.TraceContext context, int id, String name, optional boolean hasPreamble);
+ method public androidx.tracing.driver.CounterTrack CounterTrack(String name);
+ method public androidx.tracing.driver.ThreadTrack ThreadTrack(int id, String name);
+ method public androidx.tracing.driver.TraceContext getContext();
+ method public androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ property public androidx.tracing.driver.TraceContext context;
+ }
+
+ public class ThreadTrack extends androidx.tracing.driver.EventTrack {
+ ctor public ThreadTrack(int id, String name, androidx.tracing.driver.ProcessTrack process, optional boolean hasPreamble);
+ method public androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ }
+
+ public class TraceContext implements java.io.Closeable {
+ ctor public TraceContext(int sequenceId, androidx.tracing.driver.TraceSink sink, boolean isEnabled);
+ method public void close();
+ method public final void flush();
+ method public final int getSequenceId();
+ method public final androidx.tracing.driver.TraceSink getSink();
+ method public final boolean isEnabled();
+ property public final boolean isEnabled;
+ property public final int sequenceId;
+ property public final androidx.tracing.driver.TraceSink sink;
+ }
+
+ public final class TraceDriver {
+ ctor public TraceDriver(int sequenceId, androidx.tracing.driver.TraceSink sink, optional boolean isEnabled);
+ method public androidx.tracing.driver.ProcessTrack ProcessTrack(int id, String name);
+ method public androidx.tracing.driver.TraceContext getContext();
+ property public final androidx.tracing.driver.TraceContext context;
+ }
+
+ public abstract class TraceSink implements java.io.Closeable {
+ ctor public TraceSink();
+ method public abstract void emit(androidx.tracing.driver.PooledTracePacketArray pooledPacketArray);
+ method public abstract void flush();
+ }
+
+ public final class Tracing_androidKt {
+ method public static androidx.tracing.driver.ProcessTrack currentProcessTrack(android.content.Context context, androidx.tracing.driver.TraceDriver traceDriver);
+ }
+
+ public abstract class Track {
+ ctor public Track(androidx.tracing.driver.TraceContext context, boolean hasPreamble, long uuid, androidx.tracing.driver.Track? parent);
+ method public abstract androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ }
+
+}
+
diff --git a/tracing/tracing-driver/api/current.txt b/tracing/tracing-driver/api/current.txt
new file mode 100644
index 0000000..96b7959
--- /dev/null
+++ b/tracing/tracing-driver/api/current.txt
@@ -0,0 +1,97 @@
+// Signature format: 4.0
+package androidx.tracing.driver {
+
+ public final class AndroidTraceSink extends androidx.tracing.driver.TraceSink {
+ ctor public AndroidTraceSink(android.content.Context context);
+ ctor public AndroidTraceSink(android.content.Context context, java.io.File traceFile);
+ ctor public AndroidTraceSink(android.content.Context context, okio.BufferedSink bufferedSink, optional kotlin.coroutines.CoroutineContext coroutineContext);
+ method public void close();
+ method public void emit(androidx.tracing.driver.PooledTracePacketArray packetArray);
+ method public void flush();
+ }
+
+ public final class ClockSource_androidKt {
+ method public static inline long nanoTime();
+ }
+
+ public class CounterTrack extends androidx.tracing.driver.Track {
+ ctor public CounterTrack(String name, androidx.tracing.driver.Track parent, optional boolean hasPreamble);
+ method public final void emitDoubleCounterPacket(double value);
+ method public final void emitLongCounterPacket(long value);
+ method public androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ }
+
+ public abstract class EventTrack extends androidx.tracing.driver.Track {
+ ctor public EventTrack(androidx.tracing.driver.TraceContext context, boolean hasPreamble, long uuid, androidx.tracing.driver.Track? parent);
+ method public final androidx.tracing.driver.PooledTracePacket? beginPacket(String name, optional java.util.List<java.lang.Long> flowIds);
+ method public final void emitInstantPacket();
+ method public final androidx.tracing.driver.PooledTracePacket? endPacket(String name);
+ method public final inline <T> T trace(String name, kotlin.jvm.functions.Function0<? extends T> block);
+ method public final suspend <T> Object? traceFlow(String name, optional long flowId, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super T>);
+ }
+
+ public abstract class Poolable<T extends androidx.tracing.driver.Poolable<T>> {
+ method public abstract void recycle();
+ }
+
+ public final class PooledTracePacket extends androidx.tracing.driver.Poolable<androidx.tracing.driver.PooledTracePacket> {
+ method public void encodeTracePacket(com.squareup.wire.ProtoWriter writer);
+ method public void recycle();
+ }
+
+ public final class PooledTracePacketArray extends androidx.tracing.driver.Poolable<androidx.tracing.driver.PooledTracePacketArray> {
+ method public androidx.tracing.driver.PooledTracePacket?[] getPooledTracePacketArray();
+ method public void recycle();
+ property public final androidx.tracing.driver.PooledTracePacket?[] pooledTracePacketArray;
+ }
+
+ public class ProcessTrack extends androidx.tracing.driver.EventTrack {
+ ctor public ProcessTrack(androidx.tracing.driver.TraceContext context, int id, String name, optional boolean hasPreamble);
+ method public androidx.tracing.driver.CounterTrack CounterTrack(String name);
+ method public androidx.tracing.driver.ThreadTrack ThreadTrack(int id, String name);
+ method public androidx.tracing.driver.TraceContext getContext();
+ method public androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ property public androidx.tracing.driver.TraceContext context;
+ }
+
+ public class ThreadTrack extends androidx.tracing.driver.EventTrack {
+ ctor public ThreadTrack(int id, String name, androidx.tracing.driver.ProcessTrack process, optional boolean hasPreamble);
+ method public androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ }
+
+ public class TraceContext implements java.io.Closeable {
+ ctor public TraceContext(int sequenceId, androidx.tracing.driver.TraceSink sink, boolean isEnabled);
+ method public void close();
+ method public final void flush();
+ method public final int getSequenceId();
+ method public final androidx.tracing.driver.TraceSink getSink();
+ method public final boolean isEnabled();
+ property public final boolean isEnabled;
+ property public final int sequenceId;
+ property public final androidx.tracing.driver.TraceSink sink;
+ }
+
+ public final class TraceDriver {
+ ctor public TraceDriver(int sequenceId, androidx.tracing.driver.TraceSink sink, optional boolean isEnabled);
+ method public androidx.tracing.driver.ProcessTrack ProcessTrack(int id, String name);
+ method public androidx.tracing.driver.TraceContext getContext();
+ property public final androidx.tracing.driver.TraceContext context;
+ }
+
+ public abstract class TraceSink implements java.io.Closeable {
+ ctor public TraceSink();
+ method public abstract void emit(androidx.tracing.driver.PooledTracePacketArray pooledPacketArray);
+ method public abstract void flush();
+ }
+
+ public final class Tracing_androidKt {
+ method public static androidx.tracing.driver.ProcessTrack currentProcessTrack(android.content.Context context, androidx.tracing.driver.TraceDriver traceDriver);
+ }
+
+ public abstract class Track {
+ ctor public Track(androidx.tracing.driver.TraceContext context, boolean hasPreamble, long uuid, androidx.tracing.driver.Track? parent);
+ method public abstract androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ }
+
+}
+
diff --git a/tracing/tracing-driver/api/res-1.3.0-beta01.txt b/tracing/tracing-driver/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tracing/tracing-driver/api/res-1.3.0-beta01.txt
diff --git a/tracing/tracing-driver/api/res-current.txt b/tracing/tracing-driver/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tracing/tracing-driver/api/res-current.txt
diff --git a/tracing/tracing-driver/api/restricted_1.3.0-beta01.txt b/tracing/tracing-driver/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..96b7959
--- /dev/null
+++ b/tracing/tracing-driver/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,97 @@
+// Signature format: 4.0
+package androidx.tracing.driver {
+
+ public final class AndroidTraceSink extends androidx.tracing.driver.TraceSink {
+ ctor public AndroidTraceSink(android.content.Context context);
+ ctor public AndroidTraceSink(android.content.Context context, java.io.File traceFile);
+ ctor public AndroidTraceSink(android.content.Context context, okio.BufferedSink bufferedSink, optional kotlin.coroutines.CoroutineContext coroutineContext);
+ method public void close();
+ method public void emit(androidx.tracing.driver.PooledTracePacketArray packetArray);
+ method public void flush();
+ }
+
+ public final class ClockSource_androidKt {
+ method public static inline long nanoTime();
+ }
+
+ public class CounterTrack extends androidx.tracing.driver.Track {
+ ctor public CounterTrack(String name, androidx.tracing.driver.Track parent, optional boolean hasPreamble);
+ method public final void emitDoubleCounterPacket(double value);
+ method public final void emitLongCounterPacket(long value);
+ method public androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ }
+
+ public abstract class EventTrack extends androidx.tracing.driver.Track {
+ ctor public EventTrack(androidx.tracing.driver.TraceContext context, boolean hasPreamble, long uuid, androidx.tracing.driver.Track? parent);
+ method public final androidx.tracing.driver.PooledTracePacket? beginPacket(String name, optional java.util.List<java.lang.Long> flowIds);
+ method public final void emitInstantPacket();
+ method public final androidx.tracing.driver.PooledTracePacket? endPacket(String name);
+ method public final inline <T> T trace(String name, kotlin.jvm.functions.Function0<? extends T> block);
+ method public final suspend <T> Object? traceFlow(String name, optional long flowId, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super T>);
+ }
+
+ public abstract class Poolable<T extends androidx.tracing.driver.Poolable<T>> {
+ method public abstract void recycle();
+ }
+
+ public final class PooledTracePacket extends androidx.tracing.driver.Poolable<androidx.tracing.driver.PooledTracePacket> {
+ method public void encodeTracePacket(com.squareup.wire.ProtoWriter writer);
+ method public void recycle();
+ }
+
+ public final class PooledTracePacketArray extends androidx.tracing.driver.Poolable<androidx.tracing.driver.PooledTracePacketArray> {
+ method public androidx.tracing.driver.PooledTracePacket?[] getPooledTracePacketArray();
+ method public void recycle();
+ property public final androidx.tracing.driver.PooledTracePacket?[] pooledTracePacketArray;
+ }
+
+ public class ProcessTrack extends androidx.tracing.driver.EventTrack {
+ ctor public ProcessTrack(androidx.tracing.driver.TraceContext context, int id, String name, optional boolean hasPreamble);
+ method public androidx.tracing.driver.CounterTrack CounterTrack(String name);
+ method public androidx.tracing.driver.ThreadTrack ThreadTrack(int id, String name);
+ method public androidx.tracing.driver.TraceContext getContext();
+ method public androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ property public androidx.tracing.driver.TraceContext context;
+ }
+
+ public class ThreadTrack extends androidx.tracing.driver.EventTrack {
+ ctor public ThreadTrack(int id, String name, androidx.tracing.driver.ProcessTrack process, optional boolean hasPreamble);
+ method public androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ }
+
+ public class TraceContext implements java.io.Closeable {
+ ctor public TraceContext(int sequenceId, androidx.tracing.driver.TraceSink sink, boolean isEnabled);
+ method public void close();
+ method public final void flush();
+ method public final int getSequenceId();
+ method public final androidx.tracing.driver.TraceSink getSink();
+ method public final boolean isEnabled();
+ property public final boolean isEnabled;
+ property public final int sequenceId;
+ property public final androidx.tracing.driver.TraceSink sink;
+ }
+
+ public final class TraceDriver {
+ ctor public TraceDriver(int sequenceId, androidx.tracing.driver.TraceSink sink, optional boolean isEnabled);
+ method public androidx.tracing.driver.ProcessTrack ProcessTrack(int id, String name);
+ method public androidx.tracing.driver.TraceContext getContext();
+ property public final androidx.tracing.driver.TraceContext context;
+ }
+
+ public abstract class TraceSink implements java.io.Closeable {
+ ctor public TraceSink();
+ method public abstract void emit(androidx.tracing.driver.PooledTracePacketArray pooledPacketArray);
+ method public abstract void flush();
+ }
+
+ public final class Tracing_androidKt {
+ method public static androidx.tracing.driver.ProcessTrack currentProcessTrack(android.content.Context context, androidx.tracing.driver.TraceDriver traceDriver);
+ }
+
+ public abstract class Track {
+ ctor public Track(androidx.tracing.driver.TraceContext context, boolean hasPreamble, long uuid, androidx.tracing.driver.Track? parent);
+ method public abstract androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ }
+
+}
+
diff --git a/tracing/tracing-driver/api/restricted_current.txt b/tracing/tracing-driver/api/restricted_current.txt
new file mode 100644
index 0000000..96b7959
--- /dev/null
+++ b/tracing/tracing-driver/api/restricted_current.txt
@@ -0,0 +1,97 @@
+// Signature format: 4.0
+package androidx.tracing.driver {
+
+ public final class AndroidTraceSink extends androidx.tracing.driver.TraceSink {
+ ctor public AndroidTraceSink(android.content.Context context);
+ ctor public AndroidTraceSink(android.content.Context context, java.io.File traceFile);
+ ctor public AndroidTraceSink(android.content.Context context, okio.BufferedSink bufferedSink, optional kotlin.coroutines.CoroutineContext coroutineContext);
+ method public void close();
+ method public void emit(androidx.tracing.driver.PooledTracePacketArray packetArray);
+ method public void flush();
+ }
+
+ public final class ClockSource_androidKt {
+ method public static inline long nanoTime();
+ }
+
+ public class CounterTrack extends androidx.tracing.driver.Track {
+ ctor public CounterTrack(String name, androidx.tracing.driver.Track parent, optional boolean hasPreamble);
+ method public final void emitDoubleCounterPacket(double value);
+ method public final void emitLongCounterPacket(long value);
+ method public androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ }
+
+ public abstract class EventTrack extends androidx.tracing.driver.Track {
+ ctor public EventTrack(androidx.tracing.driver.TraceContext context, boolean hasPreamble, long uuid, androidx.tracing.driver.Track? parent);
+ method public final androidx.tracing.driver.PooledTracePacket? beginPacket(String name, optional java.util.List<java.lang.Long> flowIds);
+ method public final void emitInstantPacket();
+ method public final androidx.tracing.driver.PooledTracePacket? endPacket(String name);
+ method public final inline <T> T trace(String name, kotlin.jvm.functions.Function0<? extends T> block);
+ method public final suspend <T> Object? traceFlow(String name, optional long flowId, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super T>);
+ }
+
+ public abstract class Poolable<T extends androidx.tracing.driver.Poolable<T>> {
+ method public abstract void recycle();
+ }
+
+ public final class PooledTracePacket extends androidx.tracing.driver.Poolable<androidx.tracing.driver.PooledTracePacket> {
+ method public void encodeTracePacket(com.squareup.wire.ProtoWriter writer);
+ method public void recycle();
+ }
+
+ public final class PooledTracePacketArray extends androidx.tracing.driver.Poolable<androidx.tracing.driver.PooledTracePacketArray> {
+ method public androidx.tracing.driver.PooledTracePacket?[] getPooledTracePacketArray();
+ method public void recycle();
+ property public final androidx.tracing.driver.PooledTracePacket?[] pooledTracePacketArray;
+ }
+
+ public class ProcessTrack extends androidx.tracing.driver.EventTrack {
+ ctor public ProcessTrack(androidx.tracing.driver.TraceContext context, int id, String name, optional boolean hasPreamble);
+ method public androidx.tracing.driver.CounterTrack CounterTrack(String name);
+ method public androidx.tracing.driver.ThreadTrack ThreadTrack(int id, String name);
+ method public androidx.tracing.driver.TraceContext getContext();
+ method public androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ property public androidx.tracing.driver.TraceContext context;
+ }
+
+ public class ThreadTrack extends androidx.tracing.driver.EventTrack {
+ ctor public ThreadTrack(int id, String name, androidx.tracing.driver.ProcessTrack process, optional boolean hasPreamble);
+ method public androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ }
+
+ public class TraceContext implements java.io.Closeable {
+ ctor public TraceContext(int sequenceId, androidx.tracing.driver.TraceSink sink, boolean isEnabled);
+ method public void close();
+ method public final void flush();
+ method public final int getSequenceId();
+ method public final androidx.tracing.driver.TraceSink getSink();
+ method public final boolean isEnabled();
+ property public final boolean isEnabled;
+ property public final int sequenceId;
+ property public final androidx.tracing.driver.TraceSink sink;
+ }
+
+ public final class TraceDriver {
+ ctor public TraceDriver(int sequenceId, androidx.tracing.driver.TraceSink sink, optional boolean isEnabled);
+ method public androidx.tracing.driver.ProcessTrack ProcessTrack(int id, String name);
+ method public androidx.tracing.driver.TraceContext getContext();
+ property public final androidx.tracing.driver.TraceContext context;
+ }
+
+ public abstract class TraceSink implements java.io.Closeable {
+ ctor public TraceSink();
+ method public abstract void emit(androidx.tracing.driver.PooledTracePacketArray pooledPacketArray);
+ method public abstract void flush();
+ }
+
+ public final class Tracing_androidKt {
+ method public static androidx.tracing.driver.ProcessTrack currentProcessTrack(android.content.Context context, androidx.tracing.driver.TraceDriver traceDriver);
+ }
+
+ public abstract class Track {
+ ctor public Track(androidx.tracing.driver.TraceContext context, boolean hasPreamble, long uuid, androidx.tracing.driver.Track? parent);
+ method public abstract androidx.tracing.driver.PooledTracePacket? preamblePacket();
+ }
+
+}
+
diff --git a/tracing/tracing-driver/build.gradle b/tracing/tracing-driver/build.gradle
new file mode 100644
index 0000000..e48879c
--- /dev/null
+++ b/tracing/tracing-driver/build.gradle
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 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.
+ */
+
+
+import androidx.build.KotlinTarget
+import androidx.build.PlatformIdentifier
+import androidx.build.SoftwareType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("androidx.benchmark")
+ id("com.squareup.wire")
+}
+
+androidXMultiplatform {
+ jvm()
+ android()
+ defaultPlatform(PlatformIdentifier.ANDROID)
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ api(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesCore)
+ api(libs.androidx.annotation)
+
+ implementation("androidx.collection:collection:1.4.5")
+ implementation(libs.wireRuntime)
+ implementation(libs.okio)
+ }
+ }
+ commonTest {
+ dependencies {
+ implementation(libs.kotlinTestAnnotationsCommon)
+ implementation(libs.kotlinTest)
+ implementation(libs.kotlinCoroutinesCore)
+ implementation(libs.kotlinCoroutinesTest)
+ }
+ }
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ implementation(libs.kotlinTestJunit)
+ implementation(libs.truth)
+ }
+ }
+ jvmMain {
+ dependsOn(commonMain)
+ }
+ androidMain {
+ dependsOn(commonMain)
+ }
+ androidInstrumentedTest {
+ dependsOn(commonTest)
+ dependencies {
+ implementation(projectOrArtifact(":benchmark:benchmark-junit4"))
+ implementation(libs.junit)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testRules)
+ }
+ }
+ }
+}
+
+// Workarounds for Wire's plugin not setting code generation directory as task output correctly
+// See https://github.com/square/wire/issues/3199
+tasks.named("multiplatformSourceJar").configure {
+ dependsOn(tasks.named("generateCommonMainProtos"))
+}
+
+wire {
+ kotlin {
+ mutableTypes = true
+ }
+ sourcePath {
+ srcDir("src/commonMain/proto")
+ }
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ // Make sure to opt-in to expect-actual classes.
+ // https://youtrack.jetbrains.com/issue/KT-61573
+ freeCompilerArgs += [
+ "-Xexpect-actual-classes",
+ ]
+ }
+}
+
+androidx {
+ name = "Tracing Driver"
+ type = SoftwareType.SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ inceptionYear = "2024"
+ description = "AndroidX Tracing Driver"
+ metalavaK2UastEnabled = false
+ kotlinTarget = KotlinTarget.KOTLIN_2_0
+}
+
+android {
+ namespace = "androidx.tracing.driver"
+ defaultConfig {
+ minSdk = 23
+ }
+}
diff --git a/tracing/tracing-driver/src/androidInstrumentedTest/kotlin/AndroidManifest.xml b/tracing/tracing-driver/src/androidInstrumentedTest/kotlin/AndroidManifest.xml
new file mode 100644
index 0000000..c5797ba
--- /dev/null
+++ b/tracing/tracing-driver/src/androidInstrumentedTest/kotlin/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2025 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application>
+ <!-- enable profiling by shell for non-intrusive profiling tools -->
+ <profileable android:shell="true" />
+ </application>
+</manifest>
diff --git a/tracing/tracing-driver/src/androidInstrumentedTest/kotlin/androidx/tracing/driver/Sinks.android.kt b/tracing/tracing-driver/src/androidInstrumentedTest/kotlin/androidx/tracing/driver/Sinks.android.kt
new file mode 100644
index 0000000..f85ba26d
--- /dev/null
+++ b/tracing/tracing-driver/src/androidInstrumentedTest/kotlin/androidx/tracing/driver/Sinks.android.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+import android.content.Context
+import okio.blackholeSink
+import okio.buffer
+
+internal fun buildInMemorySink(context: Context): AndroidTraceSink {
+ val buffer = blackholeSink().buffer()
+ return AndroidTraceSink(context, buffer)
+}
diff --git a/tracing/tracing-driver/src/androidInstrumentedTest/kotlin/androidx/tracing/driver/TracingOverheadBenchmarkTest.kt b/tracing/tracing-driver/src/androidInstrumentedTest/kotlin/androidx/tracing/driver/TracingOverheadBenchmarkTest.kt
new file mode 100644
index 0000000..3239a1b
--- /dev/null
+++ b/tracing/tracing-driver/src/androidInstrumentedTest/kotlin/androidx/tracing/driver/TracingOverheadBenchmarkTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+import android.content.Context
+import androidx.benchmark.ExperimentalBenchmarkConfigApi
+import androidx.benchmark.MicrobenchmarkConfig
+import androidx.benchmark.TimeCapture
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.tracing.Trace
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+internal const val PROCESS_NAME = "process"
+internal const val TRACE_TAG = "work"
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalBenchmarkConfigApi::class)
+@LargeTest
+class TracingOverheadBenchmarkTest {
+
+ val config =
+ MicrobenchmarkConfig(
+ traceAppTagEnabled = true,
+ metrics = listOf(TimeCapture()),
+ )
+
+ @get:Rule val benchmarkRule = BenchmarkRule(config)
+
+ private fun buildTraceContext(sink: TraceSink): TraceContext {
+ return TraceContext(sequenceId = 1, sink = sink, isEnabled = true)
+ }
+
+ @Test
+ fun reference() {
+ benchmarkRule.measureRepeated {}
+ }
+
+ @Test
+ fun platformTracing() {
+ benchmarkRule.measureRepeated {
+ Trace.beginSection(TRACE_TAG)
+ Trace.endSection()
+ }
+ }
+
+ @Test
+ fun customTracingNoSink() {
+ val traceContext = buildTraceContext(NoOpSink())
+ val process = traceContext.ProcessTrack(id = 10, name = PROCESS_NAME)
+ traceContext.use { benchmarkRule.measureRepeated { process.trace(TRACE_TAG) {} } }
+ }
+
+ @Test
+ fun customTracingDisabled() {
+ val context = EmptyTraceContext
+ val process = context.ProcessTrack(id = 10, name = PROCESS_NAME)
+ context.use { benchmarkRule.measureRepeated { process.trace(TRACE_TAG) {} } }
+ }
+
+ @Test
+ fun customTracingInMemorySink() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val sink = buildInMemorySink(context)
+ val traceContext = buildTraceContext(sink)
+ traceContext.use {
+ val process = traceContext.ProcessTrack(id = 10, name = PROCESS_NAME)
+ var count = 0
+ benchmarkRule.measureRepeated {
+ process.trace(TRACE_TAG) {}
+ // The Benchmark measurement loop creates ~250k packets when measuring the cost
+ // of this loop. So we essentially give our sink a chance to catch up every 1000
+ // packets.
+ runWithMeasurementDisabled {
+ count += 1
+ if (count >= 1000) {
+ @SuppressWarnings("BanThreadSleep")
+ Thread.sleep(500L) // Just slow down the producer a bit
+ count = 0
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/AtomicBoolean.android.kt b/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/AtomicBoolean.android.kt
new file mode 100644
index 0000000..9446267
--- /dev/null
+++ b/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/AtomicBoolean.android.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+import java.util.concurrent.atomic.AtomicBoolean
+
+public actual typealias AtomicBoolean = AtomicBoolean
diff --git a/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/AtomicLong.android.kt b/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/AtomicLong.android.kt
new file mode 100644
index 0000000..60db600
--- /dev/null
+++ b/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/AtomicLong.android.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+import java.util.concurrent.atomic.AtomicLong
+
+public actual typealias AtomicLong = AtomicLong
diff --git a/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/ClockSource.android.kt b/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/ClockSource.android.kt
new file mode 100644
index 0000000..f6763fb
--- /dev/null
+++ b/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/ClockSource.android.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+@Suppress("NOTHING_TO_INLINE") public actual inline fun nanoTime(): Long = System.nanoTime()
diff --git a/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/Lock.android.kt b/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/Lock.android.kt
new file mode 100644
index 0000000..93ccff1
--- /dev/null
+++ b/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/Lock.android.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+import java.util.concurrent.locks.ReentrantLock
+
+public actual typealias Lock = ReentrantLock
diff --git a/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/TraceSink.android.kt b/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/TraceSink.android.kt
new file mode 100644
index 0000000..7fc7960
--- /dev/null
+++ b/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/TraceSink.android.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+import android.content.Context
+import com.squareup.wire.ProtoWriter
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
+import kotlin.coroutines.intrinsics.createCoroutineUnintercepted
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import okio.BufferedSink
+import okio.appendingSink
+import okio.buffer
+
+/** The trace sink that writes to a new file per trace session. */
+@SuppressWarnings("StreamFiles") // Accepts a BufferedSink instead.
+public class AndroidTraceSink(
+ context: Context,
+ private val bufferedSink: BufferedSink,
+ private val coroutineContext: CoroutineContext = Dispatchers.IO
+) : TraceSink() {
+
+ public constructor(
+ context: Context
+ ) : this(context = context, traceFile = context.noBackupFilesDir.perfettoTraceFile())
+
+ @SuppressWarnings("StreamFiles") // Accepting a BufferedSink instead.
+ public constructor(
+ context: Context,
+ traceFile: File,
+ ) : this(context = context, bufferedSink = traceFile.appendingSink().buffer())
+
+ // Hold on to the application context.
+ @Suppress("UNUSED") private val context = context.applicationContext
+ private val protoWriter: ProtoWriter = ProtoWriter(bufferedSink)
+
+ // There are 2 distinct mechanisms for thread safety here, and they are not necessarily in sync.
+ // The Queue by itself is thread-safe, but after we drain the queue we mark drainRequested
+ // to false (not an atomic operation). So a writer can come along and add a pooled array of
+ // trace packets. That is still okay given, those packets will get picked during the next
+ // drain request; or on flush() prior to the close() of the Sink.
+ // No packets are lost or dropped; and therefore we are still okay with this small
+ // compromise with thread safety.
+ private val queue = Queue<PooledTracePacketArray>()
+ private val drainRequested = AtomicBoolean(false)
+
+ @Volatile private var resumeDrain: Continuation<Unit>
+
+ init {
+ resumeDrain =
+ suspend {
+ coroutineContext[Job]?.invokeOnCompletion { makeDrainRequest() }
+ while (true) {
+ drainQueue() // Sets drainRequested to false on completion
+ suspendCoroutine<Unit> { continuation ->
+ resumeDrain = continuation
+ COROUTINE_SUSPENDED // Suspend
+ }
+ }
+ }
+ .createCoroutineUnintercepted(Continuation(context = coroutineContext) {})
+
+ // Kick things off and suspend
+ makeDrainRequest()
+ }
+
+ override fun emit(packetArray: PooledTracePacketArray) {
+ queue.addFirst(packetArray)
+ makeDrainRequest()
+ }
+
+ override fun flush() {
+ makeDrainRequest()
+ while (drainRequested.get()) {
+ // Await completion of the drain.
+ }
+ bufferedSink.flush()
+ }
+
+ private fun makeDrainRequest() {
+ // Only make a request if one is not already ongoing
+ if (drainRequested.compareAndSet(false, true)) {
+ resumeDrain.resume(Unit)
+ }
+ }
+
+ private fun drainQueue() {
+ while (queue.isNotEmpty()) {
+ val pooledPacketArray = queue.removeFirstOrNull()
+ if (pooledPacketArray != null) {
+ for (pooledTracePacket in pooledPacketArray.pooledTracePacketArray) {
+ if (pooledTracePacket != null) {
+ pooledTracePacket.encodeTracePacket(protoWriter)
+ pooledTracePacket.recycle()
+ }
+ }
+ pooledPacketArray.recycle()
+ }
+ }
+ drainRequested.set(false)
+ }
+
+ override fun close() {
+ makeDrainRequest()
+ bufferedSink.close()
+ }
+}
+
+private fun File.perfettoTraceFile(): File {
+ val formatter = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
+ formatter.timeZone = TimeZone.getTimeZone("UTC")
+ val traceFile = File(this, "perfetto-${formatter.format(Date())}.perfetto-trace")
+ return traceFile
+}
diff --git a/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/Tracing.android.kt b/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/Tracing.android.kt
new file mode 100644
index 0000000..ed779d6
--- /dev/null
+++ b/tracing/tracing-driver/src/androidMain/kotlin/androidx/tracing/driver/Tracing.android.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+import android.content.Context
+import android.os.Build
+import android.os.Process
+
+/** Return the [ProcessTrack] given the [Context] and the [TraceDriver] instance. */
+public fun currentProcessTrack(context: Context, traceDriver: TraceDriver): ProcessTrack {
+ val pid = Process.myPid()
+ val name =
+ if (Build.VERSION.SDK_INT >= 33) {
+ Process.myProcessName()
+ } else {
+ context.packageName
+ }
+
+ return traceDriver.ProcessTrack(id = pid, name = name)
+}
diff --git a/tracing/tracing-driver/src/androidMain/kotlin/perfetto/protos/package-info.java b/tracing/tracing-driver/src/androidMain/kotlin/perfetto/protos/package-info.java
new file mode 100644
index 0000000..69d8393
--- /dev/null
+++ b/tracing/tracing-driver/src/androidMain/kotlin/perfetto/protos/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2025 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.
+ */
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package perfetto.protos;
+
+import androidx.annotation.RestrictTo;
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/AtomicBoolean.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/AtomicBoolean.kt
new file mode 100644
index 0000000..36816ed
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/AtomicBoolean.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+public expect class AtomicBoolean(initial: Boolean) {
+ public fun get(): Boolean
+
+ public fun set(newValue: Boolean)
+
+ public fun compareAndSet(expected: Boolean, newValue: Boolean): Boolean
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/AtomicLong.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/AtomicLong.kt
new file mode 100644
index 0000000..a7d94c1
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/AtomicLong.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+public expect class AtomicLong(initial: Long) {
+ public fun incrementAndGet(): Long
+
+ public fun compareAndSet(expected: Long, actual: Long): Boolean
+
+ public fun get(): Long
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun AtomicLong.decrement(): Boolean {
+ val current = get()
+ return compareAndSet(current, current - 1)
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ClockSource.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ClockSource.kt
new file mode 100644
index 0000000..129f121
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ClockSource.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+public expect fun nanoTime(): Long
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Common.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Common.kt
new file mode 100644
index 0000000..6b8e4e9
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Common.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+internal val id = AtomicLong(42L)
+
+/** Generates a monotonically increasing [Long] value. */
+internal fun monotonicId(): Long {
+ return id.incrementAndGet()
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ContextElements.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ContextElements.kt
new file mode 100644
index 0000000..e08e91b
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ContextElements.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+import kotlin.coroutines.AbstractCoroutineContextElement
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.coroutineContext
+
+/** An [AbstractCoroutineContextElement] that can hold to the [List] of current `flowId`s.. */
+internal class FlowContextElement(internal val flowIds: List<Long>) :
+ AbstractCoroutineContextElement(KEY) {
+
+ internal companion object {
+ internal val KEY: CoroutineContext.Key<FlowContextElement> =
+ object : CoroutineContext.Key<FlowContextElement> {}
+ }
+}
+
+/** Useful in the context of structured concurrency to keep track of flows. */
+internal suspend fun obtainFlowContext(): FlowContextElement? {
+ return coroutineContext[FlowContextElement.KEY]
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/CounterTrack.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/CounterTrack.kt
new file mode 100644
index 0000000..81aeafb
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/CounterTrack.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+/** Represents a Perfetto Counter track. */
+public open class CounterTrack(
+ /** The name of the counter track */
+ private val name: String,
+ /** The parent track the counter belongs to. */
+ private val parent: Track,
+ hasPreamble: Boolean = true,
+) :
+ Track(
+ context = parent.context,
+ hasPreamble = hasPreamble,
+ uuid = monotonicId(),
+ parent = parent
+ ) {
+ override fun preamblePacket(): PooledTracePacket? {
+ val packet = context.pool.obtainTracePacket()
+ val track = context.pool.obtainTrackDescriptor()
+ val counter = context.pool.obtainCounterDescriptor()
+ packet.trackPoolableForOwnership(track)
+ packet.trackPoolableForOwnership(counter)
+ track.trackDescriptor.uuid = uuid
+ track.trackDescriptor.name = name
+ track.trackDescriptor.parent_uuid = parent.uuid
+ track.trackDescriptor.counter = counter.counterDescriptor
+ packet.tracePacket.timestamp = nanoTime()
+ packet.tracePacket.track_descriptor = track.trackDescriptor
+ return packet
+ }
+
+ public fun emitLongCounterPacket(value: Long) {
+ if (context.isEnabled) {
+ emit(longCounterPacket(value))
+ }
+ }
+
+ public fun emitDoubleCounterPacket(value: Double) {
+ if (context.isEnabled) {
+ emit(doubleCounterPacket(value))
+ }
+ }
+}
+
+// An empty counter track when tracing is disabled
+
+private const val EMPTY_COUNTER_NAME = "Empty Counter"
+
+internal class EmptyCounterTrack(process: EmptyProcessTrack) :
+ CounterTrack(name = EMPTY_COUNTER_NAME, parent = process, hasPreamble = false) {
+ override fun preamblePacket(): PooledTracePacket? = null
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/EventTrack.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/EventTrack.kt
new file mode 100644
index 0000000..ab18362
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/EventTrack.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+import kotlin.coroutines.coroutineContext
+import kotlinx.coroutines.withContext
+
+/** Entities that we can attach traces to to be visualized on the timeline with slices & flows. */
+public abstract class EventTrack(
+ /** The [TraceContext] instance. */
+ context: TraceContext,
+ /** `true` iff we need to emit some preamble packets. */
+ hasPreamble: Boolean,
+ /** The uuid for the track descriptor. */
+ uuid: Long,
+ /** The parent traceable. */
+ parent: Track?
+) : Track(context = context, hasPreamble = hasPreamble, uuid = uuid, parent = parent) {
+
+ public fun beginPacket(name: String, flowIds: List<Long> = emptyList()): PooledTracePacket? {
+ return if (!context.isEnabled) {
+ null
+ } else {
+ trackBeginPacket(name, flowIds)
+ }
+ }
+
+ public fun endPacket(name: String): PooledTracePacket? {
+ return if (!context.isEnabled) {
+ null
+ } else {
+ trackEndPacket(name)
+ }
+ }
+
+ public fun emitInstantPacket() {
+ if (context.isEnabled) {
+ instantPacket()
+ }
+ }
+
+ public inline fun <T> trace(name: String, crossinline block: () -> T): T {
+ if (context.isEnabled) {
+ val packet = beginPacket(name)
+ if (packet != null) {
+ emit(packet)
+ }
+ }
+ try {
+ return block()
+ } finally {
+ if (context.isEnabled) {
+ val packet = endPacket(name)
+ if (packet != null) {
+ emit(packet)
+ }
+ }
+ }
+ }
+
+ /** [Track] scoped async trace slices. */
+ public suspend fun <T> traceFlow(
+ name: String,
+ flowId: Long = monotonicId(),
+ block: suspend () -> T
+ ): T {
+ return if (!context.isEnabled) {
+ block()
+ } else {
+ traceFlow(name = name, flowIds = listOf(flowId), block = block)
+ }
+ }
+
+ /** [Track] scoped async trace slices. */
+ private suspend fun <T, R : Track> R.traceFlow(
+ name: String,
+ flowIds: List<Long>,
+ block: suspend () -> T
+ ): T {
+ val element = obtainFlowContext()
+ val newFlowIds =
+ if (element == null) {
+ flowIds
+ } else {
+ element.flowIds + flowIds
+ }
+ val newElement = FlowContextElement(flowIds = newFlowIds)
+ return withContext(coroutineContext + newElement) {
+ val begin = beginPacket(name = name, flowIds = newFlowIds)
+ if (begin != null) {
+ emit(begin)
+ }
+ try {
+ block()
+ } finally {
+ val end = endPacket(name)
+ if (end != null) {
+ emit(end)
+ }
+ }
+ }
+ }
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Lock.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Lock.kt
new file mode 100644
index 0000000..52d4739
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Lock.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+/** Represents a synchronization primitive that can be used. */
+public expect class Lock() {
+ public fun lock()
+
+ public fun unlock()
+}
+
+internal inline fun <T> Lock.withLock(crossinline block: () -> T): T {
+ try {
+ lock()
+ return block()
+ } finally {
+ unlock()
+ }
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Pool.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Pool.kt
new file mode 100644
index 0000000..108e9d0
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Pool.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+/**
+ * An Object pool that keeps track of scrap objects in a fixed size [ArrayDeque].
+ *
+ * @param size represents the size of the Object pool.
+ * @param factory is a function that can be used to create the instances of the Object for the pool.
+ * @param T represents an object that be re-used.
+ */
+internal class Pool<T>(
+ private val size: Int,
+ private val isDebug: Boolean,
+ private val factory: (owner: Pool<T>) -> T
+) {
+ private var counter: AtomicLong? = null
+
+ init {
+ if (isDebug) {
+ counter = AtomicLong(0L)
+ }
+ }
+
+ // This class is intentionally lock free.
+ // This is because, the only place where we recycle objects in the pool is in the TraceSink
+ // and that effectively behaves as-if it were single threaded.
+ private val scrapPool: ArrayDeque<T> = ArrayDeque(size)
+
+ init {
+ // Eagerly create the objects for the pool
+ for (i in 0 until size) {
+ scrapPool.addFirst(factory(this))
+ }
+ }
+
+ /** Obtain an instance of the object from the pool. */
+ internal fun obtain(): T {
+ if (isDebug) {
+ counter?.incrementAndGet()
+ }
+ return scrapPool.removeFirstOrNull() ?: factory(this)
+ }
+
+ internal fun release(element: T) {
+ if (isDebug) {
+ counter?.decrement()
+ }
+ if (scrapPool.size < size) {
+ scrapPool.addFirst(element)
+ }
+ }
+
+ internal fun count(): Long {
+ return counter?.get() ?: 0L
+ }
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Poolable.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Poolable.kt
new file mode 100644
index 0000000..3a0c1e3
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Poolable.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+/** Represents an object that can be reused using a [Pool]. This avoids GC churn. */
+public abstract class Poolable<T : Poolable<T>>
+internal constructor(internal open val owner: Pool<T>) {
+ /** Recycles the object, and hands it back to the pool. */
+ public abstract fun recycle()
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/PooledProtos.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/PooledProtos.kt
new file mode 100644
index 0000000..1477a12
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/PooledProtos.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+import com.squareup.wire.ProtoWriter
+import okio.ByteString
+import perfetto.protos.MutableCounterDescriptor
+import perfetto.protos.MutableProcessDescriptor
+import perfetto.protos.MutableThreadDescriptor
+import perfetto.protos.MutableTracePacket
+import perfetto.protos.MutableTrackDescriptor
+import perfetto.protos.MutableTrackEvent
+
+// Pooled versions of the underlying protos being used.
+
+internal class PooledTrackEvent(owner: Pool<PooledTrackEvent>, val trackEvent: MutableTrackEvent) :
+ Poolable<PooledTrackEvent>(owner) {
+ override fun recycle() {
+ trackEvent.name_iid = null
+ trackEvent.name = null
+ trackEvent.type = null
+ trackEvent.track_uuid = null
+ trackEvent.counter_value = null
+ trackEvent.double_counter_value = null
+ trackEvent.unknownFields = ByteString.EMPTY
+ trackEvent.categories = emptyList()
+ trackEvent.extra_counter_track_uuids = emptyList()
+ trackEvent.extra_counter_values = emptyList()
+ trackEvent.extra_double_counter_track_uuids = emptyList()
+ trackEvent.extra_double_counter_values = emptyList()
+ trackEvent.flow_ids = emptyList()
+ owner.release(this)
+ }
+}
+
+public class PooledTracePacket
+internal constructor(
+ owner: Pool<PooledTracePacket>,
+ // Internal for testing
+ internal val tracePacket: MutableTracePacket,
+ // We are keeping track of some associated Poolables this way, so they can consistently
+ // be recycled correctly. A size of `4` ought to be big enough for our needs.
+ private val nested: Array<Poolable<*>?> = arrayOfNulls(4)
+) : Poolable<PooledTracePacket>(owner) {
+ private var nestedIndex = 0
+
+ override fun recycle() {
+ for (i in 0 until nestedIndex) {
+ val poolable = nested[i]
+ poolable?.recycle()
+ nested[i] = null
+ }
+ nestedIndex = 0
+ tracePacket.timestamp = INVALID_LONG
+ tracePacket.timestamp_clock_id = null
+ tracePacket.track_event = null
+ tracePacket.track_descriptor = null
+ tracePacket.trace_uuid = null
+ tracePacket.compressed_packets = null
+ tracePacket.trusted_packet_sequence_id = null
+ tracePacket.interned_data = null
+ tracePacket.sequence_flags = null
+ tracePacket.incremental_state_cleared = null
+ tracePacket.trace_packet_defaults = null
+ tracePacket.unknownFields = ByteString.EMPTY
+ owner.release(this)
+ }
+
+ internal fun trackPoolableForOwnership(poolable: Poolable<*>) {
+ nested[nestedIndex] = poolable
+ nestedIndex += 1
+ }
+
+ public fun encodeTracePacket(writer: ProtoWriter) {
+ MutableTracePacket.ADAPTER.encodeWithTag(writer, 1, tracePacket)
+ }
+}
+
+internal class PooledTrackDescriptor(
+ owner: Pool<PooledTrackDescriptor>,
+ val trackDescriptor: MutableTrackDescriptor
+) : Poolable<PooledTrackDescriptor>(owner) {
+ override fun recycle() {
+ trackDescriptor.uuid = null
+ trackDescriptor.name = null
+ trackDescriptor.parent_uuid = null
+ trackDescriptor.process = null
+ trackDescriptor.thread = null
+ trackDescriptor.counter = null
+ trackDescriptor.disallow_merging_with_system_tracks = null
+ trackDescriptor.unknownFields = ByteString.EMPTY
+ owner.release(this)
+ }
+}
+
+internal class PooledProcessDescriptor(
+ owner: Pool<PooledProcessDescriptor>,
+ val processDescriptor: MutableProcessDescriptor
+) : Poolable<PooledProcessDescriptor>(owner) {
+ override fun recycle() {
+ processDescriptor.pid = INVALID_INT
+ processDescriptor.cmdline = emptyList()
+ processDescriptor.process_name = null
+ processDescriptor.process_labels = emptyList()
+ processDescriptor.unknownFields = ByteString.EMPTY
+ owner.release(this)
+ }
+}
+
+internal class PooledThreadDescriptor(
+ owner: Pool<PooledThreadDescriptor>,
+ val threadDescriptor: MutableThreadDescriptor
+) : Poolable<PooledThreadDescriptor>(owner) {
+ override fun recycle() {
+ threadDescriptor.pid = INVALID_INT
+ threadDescriptor.tid = INVALID_INT
+ threadDescriptor.thread_name = null
+ threadDescriptor.unknownFields = ByteString.EMPTY
+ owner.release(this)
+ }
+}
+
+internal class PooledCounterDescriptor(
+ owner: Pool<PooledCounterDescriptor>,
+ val counterDescriptor: MutableCounterDescriptor
+) : Poolable<PooledCounterDescriptor>(owner) {
+ override fun recycle() {
+ counterDescriptor.type = null
+ counterDescriptor.categories = emptyList()
+ counterDescriptor.unit = null
+ counterDescriptor.unit_name = null
+ counterDescriptor.unit_multiplier = null
+ counterDescriptor.is_incremental = null
+ counterDescriptor.unknownFields = ByteString.EMPTY
+ owner.release(this)
+ }
+}
+
+public class PooledTracePacketArray
+internal constructor(
+ owner: Pool<PooledTracePacketArray>,
+ @get:SuppressWarnings("NullableCollectionElement") // Object pooling to avoid allocations
+ public val pooledTracePacketArray: Array<PooledTracePacket?>
+) : Poolable<PooledTracePacketArray>(owner) {
+ override fun recycle() {
+ for (i in 0 until pooledTracePacketArray.size) {
+ // Don't recycle the underlying tracePacket because that will happen
+ // once we serialize the packet to the ProtoStream.
+ pooledTracePacketArray[i] = null
+ }
+ owner.release(this)
+ }
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ProcessTrack.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ProcessTrack.kt
new file mode 100644
index 0000000..04d04a7
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ProcessTrack.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+import androidx.collection.mutableScatterMapOf
+
+/** Represents a track for a process in a perfetto trace. */
+public open class ProcessTrack(
+ /** The tracing context. */
+ override val context: TraceContext,
+ /** The process id */
+ internal val id: Int,
+ /** The name of the process. */
+ internal val name: String,
+ hasPreamble: Boolean = true,
+) : EventTrack(context = context, hasPreamble = hasPreamble, uuid = monotonicId(), parent = null) {
+ internal val lock = Lock()
+ internal val threads = mutableScatterMapOf<String, ThreadTrack>()
+ internal val counters = mutableScatterMapOf<String, CounterTrack>()
+
+ override fun preamblePacket(): PooledTracePacket? {
+ val packet = context.pool.obtainTracePacket()
+ val track = context.pool.obtainTrackDescriptor()
+ val process = context.pool.obtainProcessDescriptor()
+ packet.trackPoolableForOwnership(track)
+ packet.trackPoolableForOwnership(process)
+ // Populate process details
+ process.processDescriptor.pid = id
+ process.processDescriptor.process_name = name
+ // Link
+ track.trackDescriptor.uuid = uuid
+ track.trackDescriptor.process = process.processDescriptor
+ packet.tracePacket.timestamp = nanoTime()
+ packet.tracePacket.track_descriptor = track.trackDescriptor
+ packet.tracePacket.trusted_packet_sequence_id = context.sequenceId
+ return packet
+ }
+
+ /** @return A [ThreadTrack] for a given [ProcessTrack] using the unique thread [id]. */
+ public open fun ThreadTrack(id: Int, name: String): ThreadTrack {
+ // Thread ids are only unique for lifetime of the thread and can be potentially reused.
+ // Therefore we end up combining the `name` of the thread and its `id` as a key.
+ val key = "$id/$name"
+ return threads[key]
+ ?: lock.withLock {
+ val track =
+ threads.getOrPut(key) { ThreadTrack(id = id, name = name, process = this) }
+ check(track.name == name)
+ track
+ }
+ }
+
+ /** @return A [CounterTrack] for a given [ProcessTrack] with the provided [name]. */
+ public open fun CounterTrack(name: String): CounterTrack {
+ return counters[name]
+ ?: lock.withLock {
+ counters.getOrPut(name) { CounterTrack(name = name, parent = this) }
+ }
+ }
+}
+
+// An empty process track when tracing is disabled.
+
+private const val EMPTY_PROCESS_ID = -1
+private const val EMPTY_PROCESS_NAME = "Empty Process"
+
+internal class EmptyProcessTrack(override val context: EmptyTraceContext) :
+ ProcessTrack(
+ context = context,
+ id = EMPTY_PROCESS_ID,
+ name = EMPTY_PROCESS_NAME,
+ hasPreamble = false
+ ) {
+ override fun preamblePacket(): PooledTracePacket? = null
+
+ override fun ThreadTrack(id: Int, name: String): ThreadTrack = this.context.thread
+
+ override fun CounterTrack(name: String): CounterTrack = this.context.counter
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ProtoPool.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ProtoPool.kt
new file mode 100644
index 0000000..5bc98a1
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ProtoPool.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+import perfetto.protos.MutableCounterDescriptor
+import perfetto.protos.MutableProcessDescriptor
+import perfetto.protos.MutableThreadDescriptor
+import perfetto.protos.MutableTracePacket
+import perfetto.protos.MutableTrackDescriptor
+import perfetto.protos.MutableTrackEvent
+
+// This is 4 * the total number of outstanding requests that can be emitted by a track.
+private const val TRACK_EVENT_POOL_SIZE = 4096
+private const val TRACE_PACKET_POOL_SIZE = 4096
+private const val TRACK_DESCRIPTOR_POOL_SIZE = 256
+private const val PROCESS_DESCRIPTOR_POOL_SIZE = 4
+private const val THREAD_DESCRIPTOR_POOL_SIZE = 8
+private const val COUNTER_DESCRIPTOR_POOL_SIZE = 8
+// The size of the array
+// This would mean that each pool can queue up to 32 * 32 trace packets
+internal const val TRACE_PACKET_BUFFER_SIZE = 32
+// The size of the pool
+private const val TRACE_PACKET_POOL_ARRAY_POOL_SIZE = 32
+
+internal const val INVALID_INT = -1
+internal const val INVALID_LONG = -1L
+
+/** The uber proto pool that knows how to create all the necessary protos. */
+internal class ProtoPool(internal val isDebug: Boolean) {
+
+ internal val tracePacketPool: Pool<PooledTracePacket> =
+ Pool(size = TRACE_PACKET_POOL_SIZE, isDebug = isDebug) { pool ->
+ PooledTracePacket(
+ owner = pool,
+ tracePacket = MutableTracePacket(timestamp = INVALID_LONG),
+ )
+ }
+
+ internal val tracePacketArrayPool: Pool<PooledTracePacketArray> =
+ Pool(size = TRACE_PACKET_POOL_ARRAY_POOL_SIZE, isDebug = isDebug) { pool ->
+ PooledTracePacketArray(
+ owner = pool,
+ pooledTracePacketArray = arrayOfNulls(TRACE_PACKET_BUFFER_SIZE)
+ )
+ }
+
+ internal val trackDescriptorPool =
+ Pool<PooledTrackDescriptor>(size = TRACK_DESCRIPTOR_POOL_SIZE, isDebug = isDebug) { pool ->
+ PooledTrackDescriptor(owner = pool, trackDescriptor = MutableTrackDescriptor())
+ }
+
+ internal val processDescriptorPool =
+ Pool<PooledProcessDescriptor>(size = PROCESS_DESCRIPTOR_POOL_SIZE, isDebug = isDebug) { pool
+ ->
+ PooledProcessDescriptor(
+ owner = pool,
+ processDescriptor = MutableProcessDescriptor(pid = INVALID_INT),
+ )
+ }
+
+ internal val threadDescriptorPool =
+ Pool<PooledThreadDescriptor>(size = THREAD_DESCRIPTOR_POOL_SIZE, isDebug = isDebug) { pool
+ ->
+ PooledThreadDescriptor(
+ owner = pool,
+ threadDescriptor = MutableThreadDescriptor(pid = INVALID_INT, tid = INVALID_INT),
+ )
+ }
+
+ internal val counterDescriptorPool =
+ Pool<PooledCounterDescriptor>(size = COUNTER_DESCRIPTOR_POOL_SIZE, isDebug = isDebug) { pool
+ ->
+ PooledCounterDescriptor(owner = pool, counterDescriptor = MutableCounterDescriptor())
+ }
+
+ internal val trackEventPool =
+ Pool<PooledTrackEvent>(TRACK_EVENT_POOL_SIZE, isDebug = isDebug) { pool ->
+ PooledTrackEvent(owner = pool, trackEvent = MutableTrackEvent())
+ }
+
+ fun obtainTracePacket(): PooledTracePacket {
+ val packet = tracePacketPool.obtain()
+ // Always update time when dealing with recycled packets
+ // This is only being done because `timestamp` is now a required field to avoid boxing.
+ packet.tracePacket.timestamp = nanoTime()
+ return packet
+ }
+
+ fun obtainTracePacketArray(): PooledTracePacketArray {
+ return tracePacketArrayPool.obtain()
+ }
+
+ fun obtainTrackDescriptor(): PooledTrackDescriptor {
+ return trackDescriptorPool.obtain()
+ }
+
+ fun obtainProcessDescriptor(): PooledProcessDescriptor {
+ return processDescriptorPool.obtain()
+ }
+
+ fun obtainThreadDescriptor(): PooledThreadDescriptor {
+ return threadDescriptorPool.obtain()
+ }
+
+ fun obtainCounterDescriptor(): PooledCounterDescriptor {
+ return counterDescriptorPool.obtain()
+ }
+
+ fun obtainTrackEvent(): PooledTrackEvent {
+ return trackEventPool.obtain()
+ }
+
+ // Debug only
+ fun poolableCount(): Long {
+ if (!isDebug) {
+ return 0L
+ }
+
+ var count = 0L
+ count += tracePacketPool.count()
+ count += tracePacketArrayPool.count()
+ count += trackDescriptorPool.count()
+ count += processDescriptorPool.count()
+ count += threadDescriptorPool.count()
+ count += counterDescriptorPool.count()
+ count += trackEventPool.count()
+ return count
+ }
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Queue.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Queue.kt
new file mode 100644
index 0000000..6bc2197
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Queue.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+internal const val QUEUE_CAPACITY = 64
+
+/** An actual thread safe queue implementation. */
+internal class Queue<T>(capacity: Int = QUEUE_CAPACITY) {
+ private val lock = Lock()
+ private val queue: ArrayDeque<T> = ArrayDeque(capacity)
+
+ internal fun isEmpty(): Boolean {
+ return lock.withLock { queue.isEmpty() }
+ }
+
+ internal fun isNotEmpty(): Boolean {
+ return lock.withLock { queue.isNotEmpty() }
+ }
+
+ internal val size
+ get() = { lock.withLock { queue.size } }
+
+ internal fun addFirst(value: T) {
+ lock.withLock { queue.addFirst(value) }
+ }
+
+ internal fun removeFirstOrNull(): T? {
+ return lock.withLock { queue.removeFirstOrNull() }
+ }
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Sink.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Sink.kt
new file mode 100644
index 0000000..bfab3c5
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Sink.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+import okio.Closeable
+
+/** A sink that we can write [PooledTracePacket]s to. */
+public abstract class TraceSink : Closeable {
+ public abstract fun emit(pooledPacketArray: PooledTracePacketArray)
+
+ public abstract fun flush()
+}
+
+/** An empty trace sink that does nothing. */
+internal class EmptyTraceSink : TraceSink() {
+ override fun emit(pooledPacketArray: PooledTracePacketArray) {
+ // Does nothing
+ }
+
+ override fun flush() {
+ // Does nothing
+ }
+
+ override fun close() {
+ // Does nothing
+ }
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ThreadTrack.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ThreadTrack.kt
new file mode 100644
index 0000000..23e8fd0
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/ThreadTrack.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+/** Represents a track for a Thread like construct in Perfetto. */
+public open class ThreadTrack(
+ /** The thread id. */
+ internal val id: Int,
+ /** The name of the thread. */
+ internal val name: String,
+ /** The process track that the thread belongs to. */
+ internal val process: ProcessTrack,
+ hasPreamble: Boolean = true,
+) :
+ EventTrack(
+ context = process.context,
+ hasPreamble = hasPreamble,
+ uuid = monotonicId(),
+ parent = process
+ ) {
+ override fun preamblePacket(): PooledTracePacket? {
+ val packet = context.pool.obtainTracePacket()
+ val track = context.pool.obtainTrackDescriptor()
+ val thread = context.pool.obtainThreadDescriptor()
+ packet.trackPoolableForOwnership(track)
+ packet.trackPoolableForOwnership(thread)
+ // Populate thread details
+ thread.threadDescriptor.pid = process.id
+ thread.threadDescriptor.tid = id
+ thread.threadDescriptor.thread_name = name
+ // Link
+ track.trackDescriptor.uuid = uuid
+ track.trackDescriptor.thread = thread.threadDescriptor
+ packet.tracePacket.timestamp = nanoTime()
+ packet.tracePacket.track_descriptor = track.trackDescriptor
+ packet.tracePacket.trusted_packet_sequence_id = context.sequenceId
+ return packet
+ }
+}
+
+// An empty thread track when tracing is disabled
+
+private const val EMPTY_THREAD_ID = -1
+private const val EMPTY_THREAD_NAME = "Empty Thread"
+
+internal class EmptyThreadTrack(process: EmptyProcessTrack) :
+ ThreadTrack(
+ id = EMPTY_THREAD_ID,
+ name = EMPTY_THREAD_NAME,
+ process = process,
+ hasPreamble = false
+ ) {
+ override fun preamblePacket(): PooledTracePacket? = null
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/TraceContext.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/TraceContext.kt
new file mode 100644
index 0000000..99150de
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/TraceContext.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+import androidx.collection.mutableIntObjectMapOf
+import okio.Closeable
+
+/**
+ * This is something that is only typically created once per process. All the traces emitted are
+ * managed and written into a single [TraceSink] in an optimal way based on the underlying platform.
+ */
+public open class TraceContext
+internal constructor(
+ public val sequenceId: Int,
+ /** The sink all the [PooledTracePacket]'s are written to. */
+ public val sink: TraceSink,
+ /** Is tracing enabled ? */
+ public val isEnabled: Boolean,
+ /** Debug mode */
+ // When debugging is on, we keep track of outstanding allocations in the pool,
+ // and provide useful logging to help with debugging & testing.
+ internal val isDebug: Boolean,
+) : Closeable {
+
+ public constructor(
+ sequenceId: Int,
+ sink: TraceSink,
+ isEnabled: Boolean
+ ) : this(sequenceId, sink, isEnabled, isDebug = false)
+
+ internal val lock = Lock()
+ internal val pool = ProtoPool(isDebug = isDebug)
+ internal val processes = mutableIntObjectMapOf<ProcessTrack>()
+
+ /** @return A [ProcessTrack] using the unique process [id] using the provided [TraceContext]. */
+ internal open fun ProcessTrack(id: Int, name: String): ProcessTrack {
+ val track = processes[id]
+ return track
+ ?: lock.withLock {
+ val track =
+ processes.getOrPut(id) { ProcessTrack(context = this, id = id, name = name) }
+ check(track.name == name)
+ track
+ }
+ }
+
+ /** Flushes the trace packets into the underlying [TraceSink]. */
+ public fun flush() {
+ if (isEnabled) {
+ processes.forEachValue { processTrack ->
+ processTrack.flush()
+ processTrack.threads.forEachValue { threadTrack -> threadTrack.flush() }
+ processTrack.counters.forEachValue { counterTrack -> counterTrack.flush() }
+ }
+ // Call flush() on the sink after all the tracks have been flushed.
+ sink.flush()
+ }
+ }
+
+ override fun close() {
+ flush()
+ sink.close()
+ }
+}
+
+// An empty trace context when tracing is disabled.
+
+private const val EMPTY_TRACE_CONTEXT_SEQUENCE_ID = -1
+
+internal object EmptyTraceContext :
+ TraceContext(
+ sequenceId = EMPTY_TRACE_CONTEXT_SEQUENCE_ID,
+ sink = EmptyTraceSink(),
+ isEnabled = false
+ ) {
+ internal val process = EmptyProcessTrack(this)
+ internal val thread = EmptyThreadTrack(process)
+ internal val counter = EmptyCounterTrack(process)
+
+ override fun ProcessTrack(id: Int, name: String): ProcessTrack = process
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/TraceDriver.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/TraceDriver.kt
new file mode 100644
index 0000000..97a7491
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/TraceDriver.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+/** The entry point for the tracing API. */
+public class TraceDriver(
+ private val sequenceId: Int,
+ private val sink: TraceSink,
+ private val isEnabled: Boolean = true
+) {
+ public val context: TraceContext =
+ if (isEnabled) {
+ TraceContext(sequenceId = sequenceId, sink = sink, isEnabled = true)
+ } else {
+ EmptyTraceContext
+ }
+
+ /**
+ * @param id is the Process id.
+ * @param name is the name of the Process.
+ * @return a [ProcessTrack] instance that we can associate trace packets to.
+ */
+ public fun ProcessTrack(id: Int, name: String): ProcessTrack = context.ProcessTrack(id, name)
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Tracing.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Tracing.kt
new file mode 100644
index 0000000..d57dba2
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Tracing.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+import perfetto.protos.MutableTrackEvent
+
+/** A [PooledTracePacket] that represents a starting point of a trace span. */
+internal fun <T : EventTrack> T.trackBeginPacket(
+ name: String,
+ flowIds: List<Long> = emptyList()
+): PooledTracePacket {
+ val packet = context.pool.obtainTracePacket()
+ val event = context.pool.obtainTrackEvent()
+ packet.trackPoolableForOwnership(event)
+ event.trackEvent.type = MutableTrackEvent.Type.TYPE_SLICE_BEGIN
+ event.trackEvent.track_uuid = uuid
+ event.trackEvent.name = name
+ // Wire creates an `immutableCopyOf(...) the incoming list.
+ // Only set the flowIds if they exist.
+ if (flowIds.isNotEmpty()) {
+ event.trackEvent.flow_ids = flowIds
+ }
+ packet.tracePacket.timestamp = nanoTime()
+ packet.tracePacket.track_event = event.trackEvent
+ packet.tracePacket.trusted_packet_sequence_id = context.sequenceId
+ return packet
+}
+
+/** A [PooledTracePacket] that represents an end point of a trace span. */
+internal fun <T : EventTrack> T.trackEndPacket(name: String): PooledTracePacket {
+ val packet = context.pool.obtainTracePacket()
+ val event = context.pool.obtainTrackEvent()
+ packet.trackPoolableForOwnership(event)
+ event.trackEvent.type = MutableTrackEvent.Type.TYPE_SLICE_END
+ event.trackEvent.track_uuid = uuid
+ event.trackEvent.name = name
+ packet.tracePacket.timestamp = nanoTime()
+ packet.tracePacket.track_event = event.trackEvent
+ packet.tracePacket.trusted_packet_sequence_id = context.sequenceId
+ return packet
+}
+
+internal fun <T : EventTrack> T.instantPacket(): PooledTracePacket {
+ val packet = context.pool.obtainTracePacket()
+ val event = context.pool.obtainTrackEvent()
+ packet.trackPoolableForOwnership(event)
+ event.trackEvent.type = MutableTrackEvent.Type.TYPE_INSTANT
+ event.trackEvent.track_uuid = uuid
+ packet.tracePacket.track_event = event.trackEvent
+ packet.tracePacket.timestamp = nanoTime()
+ packet.tracePacket.trusted_packet_sequence_id = context.sequenceId
+ return packet
+}
+
+internal fun CounterTrack.longCounterPacket(value: Long): PooledTracePacket {
+ val packet = context.pool.obtainTracePacket()
+ val event = context.pool.obtainTrackEvent()
+ packet.trackPoolableForOwnership(event)
+ event.trackEvent.type = MutableTrackEvent.Type.TYPE_COUNTER
+ event.trackEvent.track_uuid = uuid
+ event.trackEvent.counter_value = value
+ packet.tracePacket.track_event = event.trackEvent
+ packet.tracePacket.timestamp = nanoTime()
+ packet.tracePacket.trusted_packet_sequence_id = context.sequenceId
+ return packet
+}
+
+internal fun CounterTrack.doubleCounterPacket(value: Double): PooledTracePacket {
+ val packet = context.pool.obtainTracePacket()
+ val event = context.pool.obtainTrackEvent()
+ packet.trackPoolableForOwnership(event)
+ event.trackEvent.type = MutableTrackEvent.Type.TYPE_COUNTER
+ event.trackEvent.track_uuid = uuid
+ event.trackEvent.double_counter_value = value
+ packet.tracePacket.track_event = event.trackEvent
+ packet.tracePacket.timestamp = nanoTime()
+ packet.tracePacket.trusted_packet_sequence_id = context.sequenceId
+ return packet
+}
diff --git a/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Track.kt b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Track.kt
new file mode 100644
index 0000000..df2da31
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/kotlin/androidx/tracing/driver/Track.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+import androidx.annotation.RestrictTo
+
+/** Entities that we can attach traces to. */
+public abstract class Track(
+ /** The [TraceContext] instance. */
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public open val context: TraceContext,
+ /** `true` iff we need to emit some preamble packets. */
+ internal val hasPreamble: Boolean,
+ /** The uuid for the track descriptor. */
+ internal val uuid: Long,
+ /** The parent traceable. */
+ private val parent: Track?
+) {
+ /**
+ * Any time we emit trace packets relevant to this process. We need to make sure the necessary
+ * preamble packets that describe the process and threads are also emitted. This is used to make
+ * sure that we only do that once.
+ */
+ private val preamble: AtomicBoolean = AtomicBoolean(false)
+ private val flushRequested = AtomicBoolean(false)
+ private val queue: ArrayDeque<PooledTracePacket> = ArrayDeque(TRACE_PACKET_BUFFER_SIZE * 2)
+
+ /** @return The [PooledTracePacket] which is a preamble packet for the [Track]. */
+ public abstract fun preamblePacket(): PooledTracePacket?
+
+ internal fun emitPreamble() {
+ parent?.emitPreamble()
+ if (preamble.compareAndSet(expected = false, newValue = true)) {
+ val packet = preamblePacket()
+ if (packet != null) {
+ emit(packet)
+ }
+ }
+ }
+
+ internal fun flush() {
+ if (flushRequested.compareAndSet(expected = false, newValue = true)) {
+ transferPooledPacketArray()
+ }
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun emit(packet: PooledTracePacket) {
+ emitPreamble()
+ queue.add(packet)
+ if (queue.size >= TRACE_PACKET_BUFFER_SIZE) {
+ flush()
+ }
+ }
+
+ private fun transferPooledPacketArray() {
+ do {
+ val pooledPacketArray = context.pool.obtainTracePacketArray()
+ var i = 0
+ while (queue.isNotEmpty() && i < pooledPacketArray.pooledTracePacketArray.size) {
+ val pooledTracePacket = queue.removeFirstOrNull()
+ if (pooledTracePacket != null) {
+ pooledPacketArray.pooledTracePacketArray[i] = pooledTracePacket
+ i += 1
+ }
+ }
+ context.sink.emit(pooledPacketArray)
+ } while (queue.isNotEmpty()) // There might still be more packets.
+ flushRequested.set(newValue = false)
+ }
+}
diff --git a/tracing/tracing-driver/src/commonMain/proto/perfetto_trace.proto b/tracing/tracing-driver/src/commonMain/proto/perfetto_trace.proto
new file mode 100644
index 0000000..e7c8e72
--- /dev/null
+++ b/tracing/tracing-driver/src/commonMain/proto/perfetto_trace.proto
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+syntax = "proto2";
+
+// NOTE: We get away with a copy of the proto definitions here only because in this module
+// we are using mutable types. Otherwise benchmark's definitions and this would collide and cause
+// builds to fail. Consider re-packaging this to a separate package in the future. b/392165540
+package perfetto.protos;
+
+///////////////////////////////////////////////////////////////////////////////
+// NOTE: THIS FILE IS A MANUALLY MINIFIED VERSION OF PERFETTO_TRACE.PROTO
+///////////////////////////////////////////////////////////////////////////////
+
+message EventName {
+ optional uint64 iid = 1;
+ optional string name = 2;
+}
+
+message InternedData {
+ repeated EventName event_names = 2;
+}
+
+message TracePacketDefaults {
+ optional uint32 timestamp_clock_id = 58;
+ optional TrackEventDefaults track_event_defaults = 11;
+}
+
+// A random unique ID that identifies the trace.
+// This message has been introduced in v32. Prior to that, the UUID was
+// only (optionally) present in the TraceConfig.trace_uuid_msb/lsb fields.
+message TraceUuid {
+ optional int64 msb = 1;
+ optional int64 lsb = 2;
+}
+
+message ProcessDescriptor {
+ required int32 pid = 1; // Making this required to avoid boxing an Int
+ repeated string cmdline = 2;
+ optional string process_name = 6;
+ // Labels can be used to further describe properties of the work performed by
+ // the process. For example, these can be used by Chrome renderer process to
+ // provide titles of frames being rendered.
+ repeated string process_labels = 8;
+}
+
+message TrackEvent {
+ repeated string categories = 22;
+ oneof name_field {
+ // interned EventName.
+ uint64 name_iid = 10;
+ // non-interned variant.
+ string name = 23;
+ }
+ enum Type {
+ TYPE_SLICE_BEGIN = 1;
+ TYPE_SLICE_END = 2;
+ TYPE_INSTANT = 3;
+ TYPE_COUNTER = 4;
+ }
+ optional Type type = 9;
+ optional uint64 track_uuid = 11;
+ oneof counter_value_field {
+ int64 counter_value = 30;
+ double double_counter_value = 44;
+ }
+ repeated uint64 extra_counter_track_uuids = 31;
+ repeated int64 extra_counter_values = 12;
+ repeated uint64 extra_double_counter_track_uuids = 45;
+ repeated double extra_double_counter_values = 46;
+ repeated fixed64 flow_ids = 47;
+}
+
+message ThreadDescriptor {
+ required int32 pid = 1; // Making this required to avoid boxing an Int
+ required int32 tid = 2; // Making this required to avoid boxing an Int
+ optional string thread_name = 5;
+}
+
+message CounterDescriptor {
+ // Built-in counters, usually with special meaning in the client library,
+ // trace processor, legacy JSON format, or UI. Trace processor will infer a
+ // track name from the enum value if none is provided in TrackDescriptor.
+ enum BuiltinCounterType {
+ COUNTER_UNSPECIFIED = 0;
+
+ // Thread-scoped counters. The thread's track should be specified via
+ // |parent_uuid| in the TrackDescriptor for such a counter.
+
+ // implies UNIT_TIME_NS.
+ COUNTER_THREAD_TIME_NS = 1;
+
+ // implies UNIT_COUNT.
+ COUNTER_THREAD_INSTRUCTION_COUNT = 2;
+ }
+
+ // Type of the values for the counters - to supply lower granularity units,
+ // see also |unit_multiplier|.
+ enum Unit {
+ UNIT_UNSPECIFIED = 0;
+ UNIT_TIME_NS = 1;
+ UNIT_COUNT = 2;
+ UNIT_SIZE_BYTES = 3;
+ }
+
+ // For built-in counters (e.g. thread time). Custom user-specified counters
+ // (e.g. those emitted by TRACE_COUNTER macros of the client library)
+ // shouldn't set this, and instead provide a counter name via TrackDescriptor.
+ optional BuiltinCounterType type = 1;
+
+ // Names of categories of the counter (usually for user-specified counters).
+ // In the client library, categories are a way to turn groups of individual
+ // counters (or events) on or off.
+ repeated string categories = 2;
+
+ // Type of the counter's values. Built-in counters imply a value for this
+ // field.
+ optional Unit unit = 3;
+
+ // In order to use a unit not defined as a part of |Unit|, a free-form unit
+ // name can be used instead.
+ optional string unit_name = 6;
+
+ // Multiplication factor of this counter's values, e.g. to supply
+ // COUNTER_THREAD_TIME_NS timestamps in microseconds instead.
+ optional int64 unit_multiplier = 4;
+
+ // Whether values for this counter are provided as delta values. Only
+ // supported for counters that are emitted on a single packet-sequence (e.g.
+ // thread time). Counter values in subsequent packets on the current packet
+ // sequence will be interpreted as delta values from the sequence's most
+ // recent value for the counter. When incremental state is cleared, the
+ // counter value is considered to be reset to 0. Thus, the first value after
+ // incremental state is cleared is effectively an absolute value.
+ optional bool is_incremental = 5;
+}
+
+message TrackDescriptor {
+ optional uint64 uuid = 1;
+ optional string name = 2;
+ optional uint64 parent_uuid = 5;
+ optional ProcessDescriptor process = 3;
+ optional ThreadDescriptor thread = 4;
+ optional CounterDescriptor counter = 8;
+ optional bool disallow_merging_with_system_tracks = 9;
+}
+
+message TrackEventDefaults {
+ optional uint64 track_uuid = 11;
+}
+
+message TracePacket {
+ required uint64 timestamp = 8; // Making this required to avoid boxing
+ optional uint32 timestamp_clock_id = 58;
+ oneof data {
+ TrackEvent track_event = 11;
+ TrackDescriptor track_descriptor = 60;
+ TraceUuid trace_uuid = 89;
+ bytes compressed_packets = 50; // kept for testing purposes
+ }
+ optional uint32 trusted_packet_sequence_id = 10;
+ optional InternedData interned_data = 12;
+
+ enum SequenceFlags {
+ SEQ_UNSPECIFIED = 0;
+ SEQ_INCREMENTAL_STATE_CLEARED = 1;
+ SEQ_NEEDS_INCREMENTAL_STATE = 2;
+ };
+ optional uint32 sequence_flags = 13;
+ optional bool incremental_state_cleared = 41;
+ optional TracePacketDefaults trace_packet_defaults = 59;
+}
+
+message Trace {
+ repeated TracePacket packet = 1;
+}
diff --git a/tracing/tracing-driver/src/commonTest/kotlin/androidx/tracing/driver/Sinks.kt b/tracing/tracing-driver/src/commonTest/kotlin/androidx/tracing/driver/Sinks.kt
new file mode 100644
index 0000000..6090ccd
--- /dev/null
+++ b/tracing/tracing-driver/src/commonTest/kotlin/androidx/tracing/driver/Sinks.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+/** A sink that does very little. We simply drop the trace packets without writing it to a file. */
+class NoOpSink : TraceSink() {
+ override fun emit(packetArray: PooledTracePacketArray) {
+ for (packet in packetArray.pooledTracePacketArray) {
+ packet?.recycle()
+ }
+ packetArray.recycle()
+ }
+
+ override fun flush() {
+ // Does nothing
+ }
+
+ override fun close() {
+ // Does nothing
+ }
+}
diff --git a/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/AtomicBoolean.jvm.kt b/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/AtomicBoolean.jvm.kt
new file mode 100644
index 0000000..9446267
--- /dev/null
+++ b/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/AtomicBoolean.jvm.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+import java.util.concurrent.atomic.AtomicBoolean
+
+public actual typealias AtomicBoolean = AtomicBoolean
diff --git a/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/AtomicLong.jvm.kt b/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/AtomicLong.jvm.kt
new file mode 100644
index 0000000..60db600
--- /dev/null
+++ b/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/AtomicLong.jvm.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+import java.util.concurrent.atomic.AtomicLong
+
+public actual typealias AtomicLong = AtomicLong
diff --git a/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/ClockSource.jvm.kt b/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/ClockSource.jvm.kt
new file mode 100644
index 0000000..f6763fb
--- /dev/null
+++ b/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/ClockSource.jvm.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+@Suppress("NOTHING_TO_INLINE") public actual inline fun nanoTime(): Long = System.nanoTime()
diff --git a/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/Lock.jvm.kt b/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/Lock.jvm.kt
new file mode 100644
index 0000000..93ccff1
--- /dev/null
+++ b/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/Lock.jvm.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 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 androidx.tracing.driver
+
+import java.util.concurrent.locks.ReentrantLock
+
+public actual typealias Lock = ReentrantLock
diff --git a/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/TraceSink.jvm.kt b/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/TraceSink.jvm.kt
new file mode 100644
index 0000000..235c90c
--- /dev/null
+++ b/tracing/tracing-driver/src/jvmMain/kotlin/androidx/tracing/driver/TraceSink.jvm.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+import com.squareup.wire.ProtoWriter
+import java.io.Closeable
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.coroutineContext
+import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
+import kotlin.coroutines.intrinsics.createCoroutineUnintercepted
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import okio.BufferedSink
+import okio.appendingSink
+import okio.buffer
+
+/** The trace sink that writes to a new file per trace session. */
+public class JvmTraceSink(
+ @Suppress("UNUSED") private val baseDir: File,
+ private val coroutineContext: CoroutineContext = Dispatchers.IO
+) : TraceSink(), Closeable {
+ internal val traceFile = baseDir.perfettoTraceFile()
+ private val bufferedSink: BufferedSink = traceFile.appendingSink().buffer()
+ private val protoWriter: ProtoWriter = ProtoWriter(bufferedSink)
+
+ // There are 2 distinct mechanisms for thread safety here, and they are not necessarily in sync.
+ // The Queue by itself is thread-safe, but after we drain the queue we mark drainRequested
+ // to false (not an atomic operation). So a writer can come along and add a pooled array of
+ // trace packets. That is still okay given, those packets will get picked during the next
+ // drain request; or on flush() prior to the close() of the Sink.
+ // No packets are lost or dropped; and therefore we are still okay with this small
+ // compromise with thread safety.
+ private val queue = Queue<PooledTracePacketArray>()
+ private val drainRequested = AtomicBoolean(false)
+
+ @Volatile private var resumeDrain: Continuation<Unit>
+
+ init {
+ resumeDrain =
+ suspend {
+ coroutineContext[Job]?.invokeOnCompletion { makeDrainRequest() }
+ while (true) {
+ drainQueue() // Sets drainRequested to false on completion
+ suspendCoroutine<Unit> { continuation ->
+ resumeDrain = continuation
+ COROUTINE_SUSPENDED // Suspend
+ }
+ }
+ }
+ .createCoroutineUnintercepted(Continuation(context = coroutineContext) {})
+
+ // Kick things off and suspend
+ makeDrainRequest()
+ }
+
+ override fun emit(packetArray: PooledTracePacketArray) {
+ queue.addFirst(packetArray)
+ makeDrainRequest()
+ }
+
+ override fun flush() {
+ makeDrainRequest()
+ while (drainRequested.get()) {
+ // Await completion of the drain.
+ }
+ bufferedSink.flush()
+ }
+
+ private fun makeDrainRequest() {
+ // Only make a request if one is not already ongoing
+ if (drainRequested.compareAndSet(false, true)) {
+ resumeDrain.resume(Unit)
+ }
+ }
+
+ private fun drainQueue() {
+ while (queue.isNotEmpty()) {
+ val pooledPacketArray = queue.removeFirstOrNull()
+ if (pooledPacketArray != null) {
+ for (pooledTracePacket in pooledPacketArray.pooledTracePacketArray) {
+ if (pooledTracePacket != null) {
+ pooledTracePacket.encodeTracePacket(protoWriter)
+ pooledTracePacket.recycle()
+ }
+ }
+ pooledPacketArray.recycle()
+ }
+ }
+ drainRequested.set(false)
+ }
+
+ override fun close() {
+ makeDrainRequest()
+ bufferedSink.close()
+ }
+}
+
+private fun File.perfettoTraceFile(): File {
+ val formatter = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
+ formatter.timeZone = TimeZone.getTimeZone("UTC")
+ val traceFile = File(this, "perfetto-${formatter.format(Date())}.perfetto-trace")
+ return traceFile
+}
diff --git a/tracing/tracing-driver/src/jvmTest/kotlin/androidx/tracing/driver/Asserts.kt b/tracing/tracing-driver/src/jvmTest/kotlin/androidx/tracing/driver/Asserts.kt
new file mode 100644
index 0000000..c6c4e8ee
--- /dev/null
+++ b/tracing/tracing-driver/src/jvmTest/kotlin/androidx/tracing/driver/Asserts.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+import kotlin.test.assertNotNull
+import perfetto.protos.MutableTrackEvent
+
+internal fun List<PooledTracePacket>.trackEventPacket(
+ name: String,
+ type: MutableTrackEvent.Type? = null
+): PooledTracePacket? {
+ return find { packet ->
+ var result = packet.tracePacket.track_event?.name == name
+ if (type != null) {
+ val sameType = type == packet.tracePacket.track_event?.type
+ result = result and sameType
+ }
+ result
+ }
+}
+
+internal fun List<PooledTracePacket>.assertTraceSection(name: String) {
+ val begin = trackEventPacket(name = name, type = MutableTrackEvent.Type.TYPE_SLICE_BEGIN)
+ val end = trackEventPacket(name = name, type = MutableTrackEvent.Type.TYPE_SLICE_END)
+ assertNotNull(begin) {
+ "Cannot find a track event of type ${MutableTrackEvent.Type.TYPE_SLICE_BEGIN} for $name"
+ }
+ assertNotNull(end) {
+ "Cannot find a track event of type ${MutableTrackEvent.Type.TYPE_SLICE_END} for $name"
+ }
+}
diff --git a/tracing/tracing-driver/src/jvmTest/kotlin/androidx/tracing/driver/RecyclingTest.kt b/tracing/tracing-driver/src/jvmTest/kotlin/androidx/tracing/driver/RecyclingTest.kt
new file mode 100644
index 0000000..15a6030
--- /dev/null
+++ b/tracing/tracing-driver/src/jvmTest/kotlin/androidx/tracing/driver/RecyclingTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class RecyclingTest {
+ private val sink = NoOpSink()
+
+ private val context: TraceContext =
+ TraceContext(sequenceId = 1, sink = sink, isEnabled = true, isDebug = true)
+
+ @Test
+ internal fun testProcessTrackEvents() {
+ context.use {
+ val process = context.ProcessTrack(id = 1, name = "process")
+ val thread = process.ThreadTrack(1, "thread")
+ thread.trace("section") {}
+ }
+ assertTrue(context.isDebug)
+ assertEquals(0, context.pool.poolableCount())
+ }
+
+ @Test
+ internal fun testProcessTrackFlows() = runTest {
+ context.use {
+ val process = context.ProcessTrack(id = 1, name = "process")
+ val thread = process.ThreadTrack(1, "thread")
+ thread.traceFlow("section") {}
+ }
+ assertTrue(context.isDebug)
+ assertEquals(0, context.pool.poolableCount())
+ }
+}
diff --git a/tracing/tracing-driver/src/jvmTest/kotlin/androidx/tracing/driver/TracingDemoTest.kt b/tracing/tracing-driver/src/jvmTest/kotlin/androidx/tracing/driver/TracingDemoTest.kt
new file mode 100644
index 0000000..a1ac54c
--- /dev/null
+++ b/tracing/tracing-driver/src/jvmTest/kotlin/androidx/tracing/driver/TracingDemoTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+import java.io.File
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+
+class TracingDemoTest {
+
+ internal val random = Random(42)
+
+ internal val forkSize = 20
+ internal val multiplier = 1000
+ internal val inputSize = forkSize * multiplier
+
+ // Tracks the number of batches completed
+ internal var count = 0L
+ internal val driver: TraceDriver =
+ TraceDriver(sequenceId = 1, sink = JvmTraceSink(File("/tmp")), isEnabled = true)
+
+ @Test
+ internal fun testTracingEndToEnd() = runBlocking {
+ driver.context.use {
+ withContext(context = Dispatchers.Default) {
+ // Create a process track
+ val track = driver.ProcessTrack(id = 1, name = "TracingTest")
+ track.traceFlow("begin") { delay(20L) }
+ track.traceFlow("histograms-end-to-end") { track.computeHistograms() }
+ track.traceFlow("end") { delay(20L) }
+ }
+ }
+ }
+
+ internal suspend fun ProcessTrack.computeHistograms(): Map<Int, Int> {
+ val input = List<Int>(inputSize) { random.nextInt(0, 100_000) }
+ val batches = input.chunked(multiplier)
+ return coroutineScope {
+ val jobs = mutableListOf<Deferred<Map<Int, Int>>>()
+ batches.forEachIndexed { index, batch ->
+ jobs += async { traceFlow("histograms-batch-$index") { computeHistogram(batch) } }
+ }
+ val histograms = jobs.awaitAll()
+ val output = traceFlow("merge-histograms") { mergeHistograms(input, histograms) }
+ output
+ }
+ }
+
+ internal suspend fun ProcessTrack.computeHistogram(list: List<Int>): Map<Int, Int> {
+ val counter = CounterTrack("Batches Completed")
+ val frequency = mutableMapOf<Int, Int>()
+ for (element in list) {
+ val count = frequency[element] ?: 0
+ frequency[element] = count + 1
+ }
+ delay(random.nextLong(10L, 20L)) // Waterfall
+ count += 1
+ counter.emitLongCounterPacket(count)
+ return frequency
+ }
+
+ internal suspend fun ProcessTrack.mergeHistograms(
+ input: List<Int>,
+ histograms: List<Map<Int, Int>>,
+ ): Map<Int, Int> {
+ val frequency = mutableMapOf<Int, Int>()
+ for (element in input) {
+ var count = 0
+ for (histogram in histograms) {
+ count += histogram[element] ?: 0
+ }
+ frequency[element] = count
+ }
+ delay(random.nextLong(10L, 20L)) // Waterfall
+ return frequency
+ }
+}
diff --git a/tracing/tracing-driver/src/jvmTest/kotlin/androidx/tracing/driver/TracingTest.kt b/tracing/tracing-driver/src/jvmTest/kotlin/androidx/tracing/driver/TracingTest.kt
new file mode 100644
index 0000000..3b5ea4b
--- /dev/null
+++ b/tracing/tracing-driver/src/jvmTest/kotlin/androidx/tracing/driver/TracingTest.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2025 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 androidx.tracing.driver
+
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertContains
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.runTest
+
+class TestSink : TraceSink() {
+ internal val packets = mutableListOf<PooledTracePacket>()
+
+ override fun emit(packetArray: PooledTracePacketArray) {
+ for (packet in packetArray.pooledTracePacketArray) {
+ if (packet != null) {
+ packets += packet
+ }
+ }
+ packetArray.recycle()
+ }
+
+ override fun flush() {
+ // Does nothing
+ }
+
+ override fun close() {
+ // Does nothing
+ }
+}
+
+class TracingTest {
+ private val sink = TestSink()
+ private val context: TraceContext = TraceContext(sequenceId = 1, sink = sink, isEnabled = true)
+
+ @BeforeTest
+ fun setUp() {
+ for (packet in sink.packets) {
+ packet.recycle()
+ }
+ sink.packets.clear()
+ }
+
+ @Test
+ internal fun testProcessTrackEvents() {
+ context.use {
+ val process = context.ProcessTrack(id = 1, name = "process")
+ val thread = process.ThreadTrack(1, "thread")
+ thread.trace("section") {}
+ }
+ assertTrue(sink.packets.size == 4)
+ assertNotNull(
+ sink.packets.find {
+ it.tracePacket.track_descriptor?.process?.process_name == "process"
+ }
+ )
+ assertNotNull(
+ sink.packets.find { it.tracePacket.track_descriptor?.thread?.thread_name == "thread" }
+ )
+ sink.packets.assertTraceSection("section")
+ }
+
+ @Test
+ internal fun testCounterTrackEvents() {
+ context.use {
+ val process = context.ProcessTrack(id = 1, name = "process")
+ val counter = process.CounterTrack("counter")
+ counter.emitLongCounterPacket(10L)
+ }
+ assertTrue(sink.packets.size == 3)
+ }
+
+ @Test
+ internal fun testAsyncEventsInProcess() {
+ context.use {
+ val process = context.ProcessTrack(id = 1, name = "process")
+ process.trace("section") {}
+ process.trace("section2") {}
+ }
+ assertTrue(sink.packets.size == 5)
+ assertNotNull(
+ sink.packets.find {
+ it.tracePacket.track_descriptor?.process?.process_name == "process"
+ }
+ )
+ sink.packets.assertTraceSection("section")
+ sink.packets.assertTraceSection("section2")
+ }
+
+ @Test
+ internal fun testAsyncEventsWithFlows() = runTest {
+ context.use {
+ with(context) {
+ val process = ProcessTrack(id = 1, name = "process")
+ with(process) {
+ traceFlow("service") {
+ coroutineScope {
+ async { traceFlow(name = "method1") { delay(10) } }.await()
+ async { traceFlow(name = "method2") { delay(40) } }.await()
+ }
+ }
+ }
+ }
+ }
+ assertTrue { sink.packets.isNotEmpty() }
+ val serviceBegin = sink.packets.trackEventPacket(name = "service")
+ val method1Begin = sink.packets.trackEventPacket(name = "method1")
+ val method2Begin = sink.packets.trackEventPacket(name = "method2")
+ assertNotNull(serviceBegin) { "Cannot find packet with name service" }
+ val flowId = serviceBegin.tracePacket.track_event?.flow_ids?.first()
+ assertNotNull(flowId) { "Packet $serviceBegin does not include a flow_id" }
+ assertNotNull(method1Begin) { "Cannot find packet with name method1" }
+ assertNotNull(method2Begin) { "Cannot find packet with name method2" }
+ assertContains(method1Begin.tracePacket.track_event?.flow_ids ?: emptyList(), flowId)
+ assertContains(method2Begin.tracePacket.track_event?.flow_ids ?: emptyList(), flowId)
+ }
+}