android_sdk: add nested tracks (PerfettoTrack + usingTrack)

Immutable PerfettoTrack handle (process/thread/global root + child())
plus usingTrack(), carried by one native NestedTracks extra over the
existing HL NESTED_TRACKS ABI.

Example usage:

  static final PerfettoTrack RENDER = PerfettoTrack.process("Render");
  static final PerfettoTrack GPU = RENDER.child("GPU");
  PerfettoTrace.instant(CAT, "frame").usingTrack(GPU).emit();

Change-Id: Ie2c9248ae7bd2fcd3be256ae3974559837b4b6c1
diff --git a/Android.bp b/Android.bp
index b67784e..941254b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -22233,6 +22233,7 @@
     srcs: [
         "src/android_sdk/java/main/dev/perfetto/sdk/PerfettoNativeMemoryCleaner.java",
         "src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrace.java",
+        "src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrack.java",
         "src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrackEventBuilder.java",
         "src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrackEventExtra.java",
     ],
diff --git a/BUILD b/BUILD
index df3c8af..24c7d2f 100644
--- a/BUILD
+++ b/BUILD
@@ -6548,6 +6548,7 @@
     srcs = [
         "src/android_sdk/java/main/dev/perfetto/sdk/PerfettoNativeMemoryCleaner.java",
         "src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrace.java",
+        "src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrack.java",
         "src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrackEventBuilder.java",
         "src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrackEventExtra.java",
     ],
diff --git a/src/android_sdk/java/main/BUILD.gn b/src/android_sdk/java/main/BUILD.gn
index 6f219f5..be304c2 100644
--- a/src/android_sdk/java/main/BUILD.gn
+++ b/src/android_sdk/java/main/BUILD.gn
@@ -7,6 +7,7 @@
   sources = [
     "dev/perfetto/sdk/PerfettoNativeMemoryCleaner.java",
     "dev/perfetto/sdk/PerfettoTrace.java",
+    "dev/perfetto/sdk/PerfettoTrack.java",
     "dev/perfetto/sdk/PerfettoTrackEventBuilder.java",
     "dev/perfetto/sdk/PerfettoTrackEventExtra.java",
   ]
diff --git a/src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrack.java b/src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrack.java
new file mode 100644
index 0000000..97d73da
--- /dev/null
+++ b/src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrack.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2026 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 dev.perfetto.sdk;
+
+import com.google.errorprone.annotations.CompileTimeConstant;
+
+import dev.perfetto.sdk.PerfettoTrackEventExtra.NestedTracks;
+
+/**
+ * An immutable, reusable handle to a (possibly nested) named track.
+ *
+ * <p>Build a track once — typically a {@code static final} — and pass it to
+ * {@link PerfettoTrackEventBuilder#usingTrack}. A track is rooted at the process,
+ * the current thread, or the global scope, and can be nested arbitrarily deep with
+ * {@link #child}:
+ *
+ * <pre>
+ *   static final PerfettoTrack RENDER = PerfettoTrack.process("Render");
+ *   static final PerfettoTrack GPU = RENDER.child("GPU");
+ *   ...
+ *   PerfettoTrace.instant(CAT, "frame").usingTrack(GPU).emit();
+ * </pre>
+ *
+ * <p>Emitting on {@code GPU} emits the {@code TrackDescriptor}s for the whole
+ * chain (Render under the process, GPU under Render) once per sequence, matching
+ * the C SDK's nested-track behaviour. The track uuid is derived natively exactly
+ * as the C SDK derives it.
+ *
+ * <p>This is the nesting-only shape supported by the high-level ABI; sibling
+ * ordering, counter units and similar are intentionally not exposed here.
+ */
+public final class PerfettoTrack {
+  // Root scope of the chain. Mirrors RootType in tracing_sdk.h.
+  static final int ROOT_GLOBAL = 0;
+  static final int ROOT_PROCESS = 1;
+  static final int ROOT_THREAD = 2;
+
+  // Default per-level id: no disambiguation between same-named sibling tracks.
+  private static final long DEFAULT_ID = 0;
+
+  final int mRootType;
+  // Names and ids of the chain, outermost (closest to the root) first.
+  final String[] mNames;
+  final long[] mIds;
+
+  // The handle's native nested-tracks extra, built lazily on first use and held
+  // for the handle's lifetime. Freed by the cleaner when the handle is collected.
+  private volatile NestedTracks mNested;
+
+  // Frees a handle's native track when the handle is collected. SystemCleaner
+  // needs no native lib, so it is safe to hold statically.
+  private static final PerfettoNativeMemoryCleaner sCleaner =
+      new PerfettoNativeMemoryCleaner();
+
+  private PerfettoTrack(int rootType, String[] names, long[] ids) {
+    mRootType = rootType;
+    mNames = names;
+    mIds = ids;
+  }
+
+  /**
+   * The handle's native nested-tracks extra, built once and reused.
+   * Package-private; used by {@link PerfettoTrackEventBuilder#usingTrack}.
+   *
+   * <p>Lock-free lazy init: concurrent first callers may each build an equivalent
+   * {@code NestedTracks}, but they are read-only and content-identical (same uuid
+   * and descriptor, deduped per-sequence natively), so only one need survive. The
+   * {@code volatile} field publishes the winner; the rest are freed by the cleaner.
+   */
+  NestedTracks nestedTracks() {
+    NestedTracks n = mNested;
+    if (n == null) {
+      n = new NestedTracks(this, sCleaner);
+      mNested = n;
+    }
+    return n;
+  }
+
+  /** A track named {@code name} rooted at the process track. */
+  public static PerfettoTrack process(@CompileTimeConstant String name) {
+    return new PerfettoTrack(ROOT_PROCESS, new String[] {name}, new long[] {DEFAULT_ID});
+  }
+
+  /** A track named {@code name} rooted at the calling thread's track. */
+  public static PerfettoTrack thread(@CompileTimeConstant String name) {
+    return new PerfettoTrack(ROOT_THREAD, new String[] {name}, new long[] {DEFAULT_ID});
+  }
+
+  /** A track named {@code name} rooted at the global scope. */
+  public static PerfettoTrack global(@CompileTimeConstant String name) {
+    return new PerfettoTrack(ROOT_GLOBAL, new String[] {name}, new long[] {DEFAULT_ID});
+  }
+
+  /** A child track named {@code name} nested under this one. */
+  public PerfettoTrack child(@CompileTimeConstant String name) {
+    return child(DEFAULT_ID, name);
+  }
+
+  /**
+   * A child track named {@code name} nested under this one. {@code id} further
+   * disambiguates the track from same-named siblings.
+   */
+  public PerfettoTrack child(long id, @CompileTimeConstant String name) {
+    int n = mNames.length;
+    String[] names = new String[n + 1];
+    long[] ids = new long[n + 1];
+    System.arraycopy(mNames, 0, names, 0, n);
+    System.arraycopy(mIds, 0, ids, 0, n);
+    names[n] = name;
+    ids[n] = id;
+    return new PerfettoTrack(mRootType, names, ids);
+  }
+}
diff --git a/src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrackEventBuilder.java b/src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrackEventBuilder.java
index c02b948..3170b3c 100644
--- a/src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrackEventBuilder.java
+++ b/src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrackEventBuilder.java
@@ -358,7 +358,27 @@
     return this;
   }
 
-  /** Adds the events to a named track instead of the thread track where the event occurred. */
+  /**
+   * Emits this event on {@code track}, a (possibly nested) named track. The
+   * descriptor for each level of the chain is emitted once per sequence; the
+   * native side derives the per-level uuids. The handle owns its native track
+   * (built once on first use, reused for its lifetime), so a reused {@code track}
+   * -- the intended {@code static final} usage -- is allocation-free.
+   */
+  public PerfettoTrackEventBuilder usingTrack(PerfettoTrack track) {
+    if (!mIsCategoryEnabled) {
+      return this;
+    }
+    if (mIsDebug) {
+      checkNotBuildingProto();
+    }
+
+    // The handle owns its native track (built once, reused for its lifetime), so
+    // there is no builder-side cache to consult -- just hand it over.
+    addPerfettoPointerToExtra(track.nestedTracks());
+    return this;
+  }
+
   public PerfettoTrackEventBuilder usingNamedTrack(
           long id, @CompileTimeConstant String name, long parentUuid) {
       return usingNamedTrack(id, name, parentUuid, /* isNameStatic = */ true);
diff --git a/src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrackEventExtra.java b/src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrackEventExtra.java
index 15d75bb..c9faaca 100644
--- a/src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrackEventExtra.java
+++ b/src/android_sdk/java/main/dev/perfetto/sdk/PerfettoTrackEventExtra.java
@@ -158,6 +158,37 @@
     private static native long native_get_extra_ptr(long ptr);
   }
 
+  /**
+   * A nested chain of named tracks emitted via the HL {@code NESTED_TRACKS} extra.
+   * Built once per {@link PerfettoTrack} (cached by the builder) so the emit path
+   * stays allocation-free; the native side derives the per-level uuids and emits a
+   * {@code TrackDescriptor} for each level once per sequence.
+   */
+  static final class NestedTracks implements PerfettoPointer {
+    private final long mPtr;
+    private final long mExtraPtr;
+
+    NestedTracks(PerfettoTrack track, PerfettoNativeMemoryCleaner memoryCleaner) {
+      mPtr = native_init(track.mRootType, track.mNames, track.mIds);
+      mExtraPtr = native_get_extra_ptr(mPtr);
+      memoryCleaner.registerNativeAllocation(this, mPtr, native_delete());
+    }
+
+    @Override
+    public long getPtr() {
+      return mExtraPtr;
+    }
+
+    @FastNative
+    private static native long native_init(int rootType, String[] names, long[] ids);
+
+    @CriticalNative
+    private static native long native_delete();
+
+    @CriticalNative
+    private static native long native_get_extra_ptr(long ptr);
+  }
+
   static final class CounterTrack implements PerfettoPointer {
     private final long mPtr;
     private final long mExtraPtr;
diff --git a/src/android_sdk/java/test/dev/perfetto/sdk/test/PerfettoTraceTest.java b/src/android_sdk/java/test/dev/perfetto/sdk/test/PerfettoTraceTest.java
index dfcf63b..46399dd 100644
--- a/src/android_sdk/java/test/dev/perfetto/sdk/test/PerfettoTraceTest.java
+++ b/src/android_sdk/java/test/dev/perfetto/sdk/test/PerfettoTraceTest.java
@@ -27,9 +27,12 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import dev.perfetto.sdk.PerfettoNativeMemoryCleaner.AllocationStats;
 import dev.perfetto.sdk.PerfettoTrace;
+import dev.perfetto.sdk.PerfettoTrack;
 import dev.perfetto.sdk.PerfettoTrackEventBuilder;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import org.junit.Before;
 import org.junit.Test;
@@ -259,6 +262,89 @@
   }
 
   @Test
+  public void testNestedTrack() throws Exception {
+    TraceConfig traceConfig = getTraceConfig(FOO);
+
+    PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray());
+
+    PerfettoTrack parent = PerfettoTrack.process("parent_track");
+    PerfettoTrack child = parent.child("child_track");
+    PerfettoTrace.instant(FOO_CATEGORY, "event").usingTrack(child).emit();
+
+    Trace trace = Trace.parseFrom(session.close());
+
+    // Index every track descriptor by its uuid and capture the event's track.
+    Map<Long, TrackDescriptor> descriptorsByUuid = new HashMap<>();
+    long eventTrackUuid = 0;
+    for (TracePacket packet : trace.getPacketList()) {
+      if (packet.hasTrackDescriptor()) {
+        TrackDescriptor td = packet.getTrackDescriptor();
+        descriptorsByUuid.put(td.getUuid(), td);
+      }
+      if (packet.hasTrackEvent()
+          && TrackEvent.Type.TYPE_INSTANT.equals(packet.getTrackEvent().getType())
+          && packet.getTrackEvent().hasTrackUuid()) {
+        eventTrackUuid = packet.getTrackEvent().getTrackUuid();
+      }
+    }
+
+    // The event is on the leaf (child) track.
+    TrackDescriptor childTd = descriptorsByUuid.get(eventTrackUuid);
+    assertThat(childTd).isNotNull();
+    assertThat(childTd.getName()).isEqualTo("child_track");
+
+    // The child is nested under the parent track.
+    TrackDescriptor parentTd = descriptorsByUuid.get(childTd.getParentUuid());
+    assertThat(parentTd).isNotNull();
+    assertThat(parentTd.getName()).isEqualTo("parent_track");
+
+    // The parent track is rooted at the process track.
+    assertThat(parentTd.getParentUuid()).isEqualTo(PerfettoTrace.getProcessTrackUuid());
+  }
+
+  @Test
+  public void testGlobalNestedTrack() throws Exception {
+    TraceConfig traceConfig = getTraceConfig(FOO);
+
+    PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray());
+
+    PerfettoTrack parent = PerfettoTrack.global("global_parent");
+    PerfettoTrack child = parent.child("global_child");
+    PerfettoTrace.instant(FOO_CATEGORY, "event").usingTrack(child).emit();
+
+    Trace trace = Trace.parseFrom(session.close());
+
+    // Index every track descriptor by its uuid and capture the event's track.
+    Map<Long, TrackDescriptor> descriptorsByUuid = new HashMap<>();
+    long eventTrackUuid = 0;
+    for (TracePacket packet : trace.getPacketList()) {
+      if (packet.hasTrackDescriptor()) {
+        TrackDescriptor td = packet.getTrackDescriptor();
+        descriptorsByUuid.put(td.getUuid(), td);
+      }
+      if (packet.hasTrackEvent()
+          && TrackEvent.Type.TYPE_INSTANT.equals(packet.getTrackEvent().getType())
+          && packet.getTrackEvent().hasTrackUuid()) {
+        eventTrackUuid = packet.getTrackEvent().getTrackUuid();
+      }
+    }
+
+    // The event is on the leaf (child) track.
+    TrackDescriptor childTd = descriptorsByUuid.get(eventTrackUuid);
+    assertThat(childTd).isNotNull();
+    assertThat(childTd.getName()).isEqualTo("global_child");
+
+    // The child is nested under the global parent track.
+    TrackDescriptor parentTd = descriptorsByUuid.get(childTd.getParentUuid());
+    assertThat(parentTd).isNotNull();
+    assertThat(parentTd.getName()).isEqualTo("global_parent");
+
+    // A global root has no process/thread anchor: the outermost named level
+    // hangs off uuid 0.
+    assertThat(parentTd.getParentUuid()).isEqualTo(0);
+  }
+
+  @Test
   public void testProcessThreadNamedTrack() throws Exception {
     TraceConfig traceConfig = getTraceConfig(FOO);
 
diff --git a/src/android_sdk/jni/dev_perfetto_sdk_PerfettoTrackEventExtra.cc b/src/android_sdk/jni/dev_perfetto_sdk_PerfettoTrackEventExtra.cc
index 20920e0..d57199e 100644
--- a/src/android_sdk/jni/dev_perfetto_sdk_PerfettoTrackEventExtra.cc
+++ b/src/android_sdk/jni/dev_perfetto_sdk_PerfettoTrackEventExtra.cc
@@ -380,6 +380,39 @@
   return toJLong(track->get());
 }
 
+static jlong dev_perfetto_sdk_PerfettoTrackEventExtraNestedTracks_init(
+    JNIEnv* env,
+    jclass,
+    jint root_type,
+    jobjectArray names,
+    jlongArray ids) {
+  const jsize n = env->GetArrayLength(names);
+  std::vector<std::string> names_vec;
+  names_vec.reserve(static_cast<size_t>(n));
+  for (jsize i = 0; i < n; i++) {
+    jstring s = static_cast<jstring>(env->GetObjectArrayElement(names, i));
+    names_vec.emplace_back(StringBuffer::utf16_to_ascii(env, s));
+    env->DeleteLocalRef(s);
+  }
+  jlong* id_ptr = env->GetLongArrayElements(ids, nullptr);
+  std::vector<uint64_t> ids_vec(reinterpret_cast<const uint64_t*>(id_ptr),
+                                reinterpret_cast<const uint64_t*>(id_ptr) + n);
+  env->ReleaseLongArrayElements(ids, id_ptr, JNI_ABORT);
+  return toJLong(new sdk_for_jni::NestedTracks(
+      static_cast<sdk_for_jni::RootType>(root_type), names_vec, ids_vec));
+}
+
+static jlong dev_perfetto_sdk_PerfettoTrackEventExtraNestedTracks_delete(
+    PERFETTO_JNI_HOST_PARAMS) {
+  return toJLong(&sdk_for_jni::NestedTracks::delete_track);
+}
+
+static jlong dev_perfetto_sdk_PerfettoTrackEventExtraNestedTracks_get_extra_ptr(
+    PERFETTO_JNI_HOST_PARAMS_COMMA jlong ptr) {
+  sdk_for_jni::NestedTracks* track = toPointer<sdk_for_jni::NestedTracks>(ptr);
+  return toJLong(track->get());
+}
+
 static jlong dev_perfetto_sdk_PerfettoTrackEventExtraCounterTrack_init(
     JNIEnv* env,
     jclass,
@@ -596,6 +629,15 @@
      (void*)dev_perfetto_sdk_PerfettoTrackEventExtraNamedTrack_get_extra_ptr},
 };
 
+static const JNINativeMethod gNestedTracksMethods[] = {
+    {"native_init", "(I[Ljava/lang/String;[J)J",
+     (void*)dev_perfetto_sdk_PerfettoTrackEventExtraNestedTracks_init},
+    {"native_delete", "()J",
+     (void*)dev_perfetto_sdk_PerfettoTrackEventExtraNestedTracks_delete},
+    {"native_get_extra_ptr", "(J)J",
+     (void*)dev_perfetto_sdk_PerfettoTrackEventExtraNestedTracks_get_extra_ptr},
+};
+
 static const JNINativeMethod gCounterTrackMethods[] = {
     {"native_init", "(Ljava/lang/String;JZ)J",
      (void*)dev_perfetto_sdk_PerfettoTrackEventExtraCounterTrack_init},
@@ -671,6 +713,14 @@
   res = jniRegisterNativeMethods(
       env,
       TO_MAYBE_JAR_JAR_CLASS_NAME(
+          "dev/perfetto/sdk/PerfettoTrackEventExtra$NestedTracks"),
+      gNestedTracksMethods, NELEM(gNestedTracksMethods));
+  LOG_ALWAYS_FATAL_IF(res < 0,
+                      "Unable to register nested tracks native methods.");
+
+  res = jniRegisterNativeMethods(
+      env,
+      TO_MAYBE_JAR_JAR_CLASS_NAME(
           "dev/perfetto/sdk/PerfettoTrackEventExtra$CounterTrack"),
       gCounterTrackMethods, NELEM(gCounterTrackMethods));
   LOG_ALWAYS_FATAL_IF(res < 0,
diff --git a/src/android_sdk/perfetto_sdk_for_jni/tracing_sdk.cc b/src/android_sdk/perfetto_sdk_for_jni/tracing_sdk.cc
index cc85f57..4b4b3aa 100644
--- a/src/android_sdk/perfetto_sdk_for_jni/tracing_sdk.cc
+++ b/src/android_sdk/perfetto_sdk_for_jni/tracing_sdk.cc
@@ -157,6 +157,49 @@
   delete ptr;
 }
 
+NestedTracks::NestedTracks(RootType root_type,
+                           const std::vector<std::string>& names,
+                           const std::vector<uint64_t>& ids)
+    : names_(names), root_{} {
+  const size_t count = names_.size();
+  named_.reserve(count);
+  ptrs_.reserve(count + 2);
+
+  // Outermost entry: the root scope. A process or thread root prepends one
+  // entry; a global root has none -- its first named level hangs off uuid 0.
+  switch (root_type) {
+    case RootType::kProcess:
+      root_.type = PERFETTO_TE_HL_NESTED_TRACK_TYPE_PROCESS;
+      ptrs_.push_back(&root_);
+      break;
+    case RootType::kThread:
+      root_.type = PERFETTO_TE_HL_NESTED_TRACK_TYPE_THREAD;
+      ptrs_.push_back(&root_);
+      break;
+    case RootType::kGlobal:
+      break;  // No root entry; the chain hangs off uuid 0.
+  }
+
+  for (size_t i = 0; i < count; i++) {
+    PerfettoTeHlNestedTrackNamed entry{};
+    entry.header.type = PERFETTO_TE_HL_NESTED_TRACK_TYPE_NAMED;
+    entry.name = names_[i].c_str();
+    entry.id = ids[i];
+    named_.push_back(entry);
+  }
+  for (size_t i = 0; i < count; i++) {
+    ptrs_.push_back(reinterpret_cast<PerfettoTeHlNestedTrack*>(&named_[i]));
+  }
+  ptrs_.push_back(nullptr);
+
+  extra_.header.type = PERFETTO_TE_HL_EXTRA_TYPE_NESTED_TRACKS;
+  extra_.tracks = ptrs_.data();
+}
+
+void NestedTracks::delete_track(NestedTracks* ptr) {
+  delete ptr;
+}
+
 RegisteredTrack::RegisteredTrack(uint64_t id,
                                  uint64_t parent_uuid,
                                  const std::string& name,
diff --git a/src/android_sdk/perfetto_sdk_for_jni/tracing_sdk.h b/src/android_sdk/perfetto_sdk_for_jni/tracing_sdk.h
index 5d06d9e..34547cd 100644
--- a/src/android_sdk/perfetto_sdk_for_jni/tracing_sdk.h
+++ b/src/android_sdk/perfetto_sdk_for_jni/tracing_sdk.h
@@ -179,6 +179,42 @@
   PerfettoTeHlExtraNamedTrack track_;
 };
 
+// Root scope of a nested-track chain. Values match PerfettoTrack (Java) and are
+// passed verbatim over JNI.
+enum class RootType : int {
+  kGlobal = 0,
+  kProcess = 1,
+  kThread = 2,
+};
+
+/**
+ * @brief A nested chain of named tracks (the HL NESTED_TRACKS extra).
+ *
+ * The chain is, outermost first: an optional root (process or thread; global
+ * roots have none) followed by one named level per name. The native HL path
+ * derives the per-level uuids and emits a descriptor for each level.
+ */
+class NestedTracks {
+ public:
+  NestedTracks(RootType root_type,
+               const std::vector<std::string>& names,
+               const std::vector<uint64_t>& ids);
+
+  static void delete_track(NestedTracks* track);
+
+  const PerfettoTeHlExtraNestedTracks* get() const { return &extra_; }
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(NestedTracks);
+  // ptrs_ point into named_/root_ and named_[i].name into names_; nothing is
+  // resized after construction, so the pointers stay valid.
+  std::vector<std::string> names_;
+  PerfettoTeHlNestedTrack root_;
+  std::vector<PerfettoTeHlNestedTrackNamed> named_;
+  std::vector<PerfettoTeHlNestedTrack*> ptrs_;
+  PerfettoTeHlExtraNestedTracks extra_;
+};
+
 /**
  * @brief Represents a registered track.
  */