Merge "install-build-deps: Add options to override autodetected platform" into main
diff --git a/Android.bp b/Android.bp
index d668c2b..87e01a1 100644
--- a/Android.bp
+++ b/Android.bp
@@ -2842,6 +2842,9 @@
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_layers_config.gen.cc",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_transactions_config.gen.cc",
     ],
+    tool_files: [
+        "protos/perfetto/common/android_log_constants.proto",
+    ],
 }
 
 // GN: //protos/perfetto/config/android:cpp
@@ -2878,6 +2881,9 @@
         ".",
         "protos",
     ],
+    tool_files: [
+        "protos/perfetto/common/android_log_constants.proto",
+    ],
 }
 
 // GN: //protos/perfetto/config/android:lite
@@ -2909,6 +2915,9 @@
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_layers_config.pb.cc",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_transactions_config.pb.cc",
     ],
+    tool_files: [
+        "protos/perfetto/common/android_log_constants.proto",
+    ],
 }
 
 // GN: //protos/perfetto/config/android:lite
@@ -2944,6 +2953,9 @@
         ".",
         "protos",
     ],
+    tool_files: [
+        "protos/perfetto/common/android_log_constants.proto",
+    ],
 }
 
 // GN: //protos/perfetto/config/android:zero
@@ -2976,6 +2988,9 @@
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_layers_config.pbzero.cc",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_transactions_config.pbzero.cc",
     ],
+    tool_files: [
+        "protos/perfetto/common/android_log_constants.proto",
+    ],
 }
 
 // GN: //protos/perfetto/config/android:zero
@@ -3012,6 +3027,9 @@
         ".",
         "protos",
     ],
+    tool_files: [
+        "protos/perfetto/common/android_log_constants.proto",
+    ],
 }
 
 // GN: //protos/perfetto/config:cpp
diff --git a/protos/perfetto/config/chrome/scenario_config.proto b/protos/perfetto/config/chrome/scenario_config.proto
index 5094f82..e90d6db 100644
--- a/protos/perfetto/config/chrome/scenario_config.proto
+++ b/protos/perfetto/config/chrome/scenario_config.proto
@@ -29,9 +29,16 @@
   // triggered.
   optional float trigger_chance = 2;
 
-  // Additional delay on the trigger below.
+  // Additional delay *after* the trigger below. This is mostly useful
+  // to trace beyond a triggered event in upload rules. Other triggers
+  // can still be serviced during this period.
   optional uint64 delay_ms = 3;
 
+  // Delay *before* which the rule is activated. Trigger events during this
+  // period are ignored by this rule. This is mostly useful to trace for a
+  // minimum duration before watching trigger events.
+  optional uint64 activation_delay_ms = 8;
+
   // Triggers when a value within the specified bounds [min_value,
   // max_value] is emitted into a Chrome histogram.
   message HistogramTrigger {
diff --git a/protos/perfetto/trace/test_extensions.proto b/protos/perfetto/trace/test_extensions.proto
index 0d33c09..2d21e92 100644
--- a/protos/perfetto/trace/test_extensions.proto
+++ b/protos/perfetto/trace/test_extensions.proto
@@ -28,6 +28,7 @@
 message TestExtension {
   extend TrackEvent {
     optional string string_extension_for_testing = 9900;
+    optional string string_extension_for_testing2 = 9905;
     repeated int32 int_extension_for_testing = 9901;
     optional string omitted_extension_for_testing = 9902;
     optional TestExtensionChild nested_message_extension_for_testing = 9903;
diff --git a/src/protozero/protoc_plugin/protozero_plugin.cc b/src/protozero/protoc_plugin/protozero_plugin.cc
index 55c3c9d..e4c691f 100644
--- a/src/protozero/protoc_plugin/protozero_plugin.cc
+++ b/src/protozero/protoc_plugin/protozero_plugin.cc
@@ -108,6 +108,8 @@
   void SetOption(const std::string& name, const std::string& value) {
     if (name == "wrapper_namespace") {
       wrapper_namespace_ = value;
+    } else if (name == "sdk") {
+      sdk_mode_ = (value == "true" || value == "1");
     } else {
       Abort(std::string() + "Unknown plugin option '" + name + "'.");
     }
@@ -468,27 +470,37 @@
         "#ifndef $guard$\n"
         "#define $guard$\n\n"
         "#include <stddef.h>\n"
-        "#include <stdint.h>\n\n"
-        "#include \"perfetto/protozero/field_writer.h\"\n"
-        "#include \"perfetto/protozero/message.h\"\n"
-        "#include \"perfetto/protozero/packed_repeated_fields.h\"\n"
-        "#include \"perfetto/protozero/proto_decoder.h\"\n"
-        "#include \"perfetto/protozero/proto_utils.h\"\n",
+        "#include <stdint.h>\n\n",
         "greeting", greeting, "guard", guard);
 
-    // Print includes for public imports.
-    for (const FileDescriptor* dependency : public_imports_) {
-      // Dependency name could contain slashes but importing from upper-level
-      // directories is not possible anyway since build system processes each
-      // proto file individually. Hence proto lookup path is always equal to the
-      // directory where particular proto file is located and protoc does not
-      // allow reference to upper directory (aka ..) in import path.
-      //
-      // Laconically said:
-      // - source_->name() may never have slashes,
-      // - dependency->name() may have slashes but always refers to inner path.
-      stub_h_->Print("#include \"$name$.h\"\n", "name",
-                     ProtoStubName(dependency));
+    if (sdk_mode_) {
+      stub_h_->Print("#include \"perfetto.h\"\n");
+    } else {
+      stub_h_->Print(
+          "#include \"perfetto/protozero/field_writer.h\"\n"
+          "#include \"perfetto/protozero/message.h\"\n"
+          "#include \"perfetto/protozero/packed_repeated_fields.h\"\n"
+          "#include \"perfetto/protozero/proto_decoder.h\"\n"
+          "#include \"perfetto/protozero/proto_utils.h\"\n");
+    }
+
+    // Print includes for public imports. In sdk mode, all imports are assumed
+    // to be part of the sdk.
+    if (!sdk_mode_) {
+      for (const FileDescriptor* dependency : public_imports_) {
+        // Dependency name could contain slashes but importing from upper-level
+        // directories is not possible anyway since build system processes each
+        // proto file individually. Hence proto lookup path is always equal to
+        // the directory where particular proto file is located and protoc does
+        // not allow reference to upper directory (aka ..) in import path.
+        //
+        // Laconically said:
+        // - source_->name() may never have slashes,
+        // - dependency->name() may have slashes but always refers to inner
+        // path.
+        stub_h_->Print("#include \"$name$.h\"\n", "name",
+                       ProtoStubName(dependency));
+      }
     }
     stub_h_->Print("\n");
 
@@ -1003,6 +1015,9 @@
   std::vector<const EnumDescriptor*> enums_;
   std::map<std::string, std::vector<const FieldDescriptor*>> extensions_;
 
+  // Generate headers that can be used with the Perfetto SDK.
+  bool sdk_mode_ = false;
+
   // The custom *Comp comparators are to ensure determinism of the generator.
   std::set<const FileDescriptor*, FileDescriptorComp> public_imports_;
   std::set<const FileDescriptor*, FileDescriptorComp> private_imports_;
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_slice_names.sql b/src/trace_processor/metrics/sql/chrome/chrome_slice_names.sql
index 95ea995..3aa7167 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_slice_names.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_slice_names.sql
@@ -27,6 +27,7 @@
   'slice_name', (
     SELECT RepeatedField(DISTINCT(name))
     FROM slice
+    WHERE name IS NOT NULL
     ORDER BY name
   )
 );
diff --git a/src/trace_processor/rpc/BUILD.gn b/src/trace_processor/rpc/BUILD.gn
index 8604153..26b7528 100644
--- a/src/trace_processor/rpc/BUILD.gn
+++ b/src/trace_processor/rpc/BUILD.gn
@@ -23,9 +23,9 @@
 # interface) and by the :httpd module for the HTTP interface.
 source_set("rpc") {
   sources = [
+    "query_result_serializer.cc",
     "rpc.cc",
     "rpc.h",
-    "query_result_serializer.cc",
   ]
   deps = [
     "..:lib",
@@ -44,8 +44,10 @@
 }
 
 # Static library target for RPC code. Needed for BigTrace in Google3.
-static_library("trace_processor_rpc") {
-  public_deps = [ ":rpc" ]
+if (is_perfetto_build_generator) {
+  static_library("trace_processor_rpc") {
+    public_deps = [ ":rpc" ]
+  }
 }
 
 perfetto_unittest_source_set("unittests") {
diff --git a/src/trace_processor/util/descriptors.cc b/src/trace_processor/util/descriptors.cc
index bd365f8..96833ed 100644
--- a/src/trace_processor/util/descriptors.cc
+++ b/src/trace_processor/util/descriptors.cc
@@ -239,7 +239,8 @@
     const std::string file_name = file.name().ToStdString();
     if (base::StartsWithAny(file_name, skip_prefixes))
       continue;
-    if (processed_files_.find(file_name) != processed_files_.end()) {
+    if (!merge_existing_messages &&
+        processed_files_.find(file_name) != processed_files_.end()) {
       // This file has been loaded once already. Skip.
       continue;
     }
diff --git a/src/tracing/internal/track_event_internal.cc b/src/tracing/internal/track_event_internal.cc
index 5d67ad8..c594adb 100644
--- a/src/tracing/internal/track_event_internal.cc
+++ b/src/tracing/internal/track_event_internal.cc
@@ -53,6 +53,7 @@
 static constexpr const char kLegacySlowPrefix[] = "disabled-by-default-";
 static constexpr const char kSlowTag[] = "slow";
 static constexpr const char kDebugTag[] = "debug";
+static constexpr const char kFilteredEventName[] = "FILTERED";
 
 constexpr auto kClockIdIncremental =
     TrackEventIncrementalState::kClockIdIncremental;
@@ -515,7 +516,10 @@
 void TrackEventInternal::WriteEventName(perfetto::DynamicString event_name,
                                         perfetto::EventContext& event_ctx,
                                         const TrackEventTlsState& tls_state) {
-  if (PERFETTO_LIKELY(!tls_state.filter_dynamic_event_names)) {
+  if (PERFETTO_UNLIKELY(tls_state.filter_dynamic_event_names)) {
+    event_ctx.event()->set_name(kFilteredEventName,
+                                sizeof(kFilteredEventName) - 1);
+  } else {
     event_ctx.event()->set_name(event_name.value, event_name.length);
   }
 }
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index 0660ada..707e775 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -3560,7 +3560,8 @@
     auto slices = StopSessionAndReadSlicesFromTrace(tracing_session);
     ASSERT_EQ(3u, slices.size());
     EXPECT_EQ("B:test.Event1", slices[0]);
-    EXPECT_EQ(filter_dynamic_names ? "B:test" : "B:test.Event2", slices[1]);
+    EXPECT_EQ(filter_dynamic_names ? "B:test.FILTERED" : "B:test.Event2",
+              slices[1]);
     EXPECT_EQ("B:test.Event3", slices[2]);
   }
 }
diff --git a/test/trace_processor/diff_tests/track_event/track_event_typed_args.textproto b/test/trace_processor/diff_tests/track_event/track_event_typed_args.textproto
index b2d9208..a85ef2c 100644
--- a/test/trace_processor/diff_tests/track_event/track_event_typed_args.textproto
+++ b/test/trace_processor/diff_tests/track_event/track_event_typed_args.textproto
@@ -72,6 +72,8 @@
     }
     [perfetto.protos.TestExtension.string_extension_for_testing]:
         "an extension string!"
+    [perfetto.protos.TestExtension.string_extension_for_testing2]:
+        "a second extension string!"
     [perfetto.protos.TestExtension.int_extension_for_testing]: 42
     [perfetto.protos.TestExtension.int_extension_for_testing]: 1337
     [perfetto.protos.TestExtension.omitted_extension_for_testing]:
@@ -99,6 +101,7 @@
     extension_set {
       file {
         package: "perfetto.protos"
+        name: "test_track_event_extensions.proto"
         message_type {
           extension {
             name: "string_extension_for_testing"
@@ -143,6 +146,30 @@
     }
   }
 }
+# Test that a field specified in a second descriptor for the same proto file is
+# also detected. This emulates the case of multiple instances of the same app
+# (e.g. Chrome Beta + Chrome Canary) both emitting a descriptor.
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 5001
+  extension_descriptor {
+    extension_set {
+      file {
+        package: "perfetto.protos"
+        name: "test_track_event_extensions.proto"
+        message_type {
+          extension {
+            name: "string_extension_for_testing2"
+            extendee: ".perfetto.protos.TrackEvent"
+            number: 9905
+            type: TYPE_STRING
+            label: LABEL_OPTIONAL
+          }
+        }
+      }
+    }
+  }
+}
 packet {
   trusted_packet_sequence_id: 1
   timestamp: 6000
diff --git a/test/trace_processor/diff_tests/track_event/track_event_typed_args_args.out b/test/trace_processor/diff_tests/track_event/track_event_typed_args_args.out
index cb6ade4..47a173a 100644
--- a/test/trace_processor/diff_tests/track_event/track_event_typed_args_args.out
+++ b/test/trace_processor/diff_tests/track_event/track_event_typed_args_args.out
@@ -37,3 +37,4 @@
 "source_id","source_id",1,"[NULL]"
 "source_location_iid","source_location_iid",1,"[NULL]"
 "string_extension_for_testing","string_extension_for_testing","[NULL]","an extension string!"
+"string_extension_for_testing2","string_extension_for_testing2","[NULL]","a second extension string!"
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index 552c7d3..176a8d5 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -182,6 +182,15 @@
 # The directory where the generated perfetto_build_flags.h will be copied into.
 buildflags_dir = 'include/perfetto/base/build_configs/android_tree'
 
+# Map of protos to transitive proto deps
+# When generating Android.bp files, these transitive deps will be added to
+# `tool_files` so that aprotoc can run inside a sandbox.
+# TODO(b/304495403): This should not be manually generated
+proto_to_transitive_proto_deps = {
+    'protos/perfetto/config/android/android_log_config.proto': [
+        'protos/perfetto/common/android_log_constants.proto',
+    ],
+}
 
 def enumerate_data_deps():
   with open(os.path.join(ROOT_DIR, 'tools', 'test_data.txt')) as f:
@@ -745,11 +754,20 @@
   blueprint.add_module(source_module)
   source_module.srcs.update(
       gn_utils.label_to_path(src) for src in target.sources)
+  # Add the imported proto file as a tool_file dep so that this action can be
+  # sandboxed.
+  tool_files = []
+  for proto, transitive_proto_deps in proto_to_transitive_proto_deps.items():
+    if proto in source_module.srcs:
+      tool_files.extend(transitive_proto_deps)
+
+  source_module.tool_files = tool_files
 
   header_module = Module('genrule', source_module_name + '_headers',
                          target.name)
   blueprint.add_module(header_module)
   header_module.srcs = set(source_module.srcs)
+  header_module.tool_files = tool_files
 
   # TODO(primiano): at some point we should remove this. This was introduced
   # by aosp/1108421 when adding "protos/" to .proto include paths, in order to
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 0999b31..022f01f 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -294,6 +294,7 @@
       args: {
         engineId: string; name: string; id: string; summaryTrackId: string;
         collapsed: boolean;
+        fixedOrdering?: boolean;
       }): void {
     state.trackGroups[args.id] = {
       engineId: args.engineId,
@@ -301,6 +302,7 @@
       id: args.id,
       collapsed: args.collapsed,
       tracks: [args.summaryTrackId],
+      fixedOrdering: args.fixedOrdering,
     };
   },
 
@@ -379,6 +381,8 @@
     // rather than T1, T10, T11, ..., T2, T20, T21 .
     const coll = new Intl.Collator([], {sensitivity: 'base', numeric: true});
     for (const group of Object.values(state.trackGroups)) {
+      if (group.fixedOrdering) continue;
+
       group.tracks.sort((a: string, b: string) => {
         const aRank = getFullKey(a);
         const bRank = getFullKey(b);
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 1a8c68e..798d7dc 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -241,6 +241,7 @@
   collapsed: boolean;
   tracks: string[];  // Child track ids.
   state?: unknown;
+  fixedOrdering?: boolean;  // Render tracks without sorting.
 }
 
 export interface EngineConfig {
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index b4a8228..a7cec8a 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -43,7 +43,6 @@
 import {
   ENABLE_SCROLL_JANK_PLUGIN_V2,
   getScrollJankTracks,
-  INPUT_LATENCY_TRACK,
 } from '../tracks/chrome_scroll_jank';
 import {
   decideTracks as scrollJankDecideTracks,
@@ -186,18 +185,6 @@
     }
   }
 
-  async addScrollJankTracks(engine: Engine): Promise<void> {
-    const scrollJankTracks = getScrollJankTracks(engine);
-    const scrollJankTracksResult = await scrollJankTracks;
-    const originalLength = this.tracksToAdd.length;
-    this.tracksToAdd.length += scrollJankTracksResult.tracksToAdd.length;
-
-    for (let i = 0; i < scrollJankTracksResult.tracksToAdd.length; ++i) {
-      this.tracksToAdd[i + originalLength] =
-          scrollJankTracksResult.tracksToAdd[i];
-    }
-  }
-
   async addCpuFreqTracks(engine: EngineProxy): Promise<void> {
     const cpus = await this.engine.getCpus();
 
@@ -281,7 +268,6 @@
     });
 
     const parentIdToGroupId = new Map<number, string>();
-    let scrollJankRendered = false;
 
     for (; it.valid(); it.next()) {
       const kind = ASYNC_SLICE_TRACK_KIND;
@@ -330,13 +316,6 @@
         }
       }
 
-      if (ENABLE_SCROLL_JANK_PLUGIN_V2.get() && !scrollJankRendered &&
-          name.includes(INPUT_LATENCY_TRACK)) {
-        // This ensures that the scroll jank tracks render above the tracks
-        // for GestureScrollUpdate.
-        await this.addScrollJankTracks(this.engine);
-        scrollJankRendered = true;
-      }
       const track = {
         engineId: this.engineId,
         kind,
@@ -1777,6 +1756,21 @@
     }
   }
 
+  async addScrollJankPluginTracks(): Promise<void> {
+    if (ENABLE_SCROLL_JANK_PLUGIN_V2.get()) {
+      const scrollJankTracksResult = await getScrollJankTracks(this.engine);
+      const tracks = scrollJankTracksResult.tracks;
+      const originalLength = this.tracksToAdd.length;
+      this.tracksToAdd.length += tracks.tracksToAdd.length;
+
+      for (let i = 0; i < tracks.tracksToAdd.length; ++i) {
+        this.tracksToAdd[i + originalLength] = tracks.tracksToAdd[i];
+      }
+
+      this.addTrackGroupActions.push(scrollJankTracksResult.addTrackGroup);
+    }
+  }
+
   async decideTracks(): Promise<DeferredAction[]> {
     await this.defineMaxLayoutDepthSqlFunction();
 
@@ -1789,6 +1783,7 @@
     }
 
     // Add first the global tracks that don't require per-process track groups.
+    await this.addScrollJankPluginTracks();
     await this.addCpuSchedulingTracks();
     await this.addFtraceTrack(
         this.engine.getProxy('TrackDecider::addFtraceTrack'));
diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
index cfcaada..42ecf77 100644
--- a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
@@ -21,7 +21,6 @@
 import {
   generateSqlWithInternalLayout,
 } from '../../common/internal_layout_utils';
-import {SCROLLING_TRACK_GROUP} from '../../common/state';
 import {globals} from '../../frontend/globals';
 import {
   NamedSliceTrackTypes,
@@ -36,6 +35,7 @@
 
 import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
 import {
+  SCROLL_JANK_GROUP_ID,
   ScrollJankPluginState,
   ScrollJankTracks as DecideTracksResult,
 } from './index';
@@ -194,7 +194,7 @@
     trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
     name: 'Chrome Scroll Input Latencies',
     config: {baseTable: baseTable},
-    trackGroup: SCROLLING_TRACK_GROUP,
+    trackGroup: SCROLL_JANK_GROUP_ID,
   });
 
   return result;
diff --git a/ui/src/tracks/chrome_scroll_jank/index.ts b/ui/src/tracks/chrome_scroll_jank/index.ts
index 592d9a5..5494921 100644
--- a/ui/src/tracks/chrome_scroll_jank/index.ts
+++ b/ui/src/tracks/chrome_scroll_jank/index.ts
@@ -12,12 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {AddTrackArgs} from '../../common/actions';
+import {v4 as uuidv4} from 'uuid';
+
+import {Actions, AddTrackArgs, DeferredAction} from '../../common/actions';
 import {Engine} from '../../common/engine';
 import {featureFlags} from '../../common/feature_flags';
 import {ObjectById} from '../../common/state';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
+import {
+  Plugin,
+  PluginContext,
+  PluginDescriptor,
+  PrimaryTrackSortKey,
+} from '../../public';
 import {CustomSqlDetailsPanelConfig} from '../custom_sql_table_slices';
+import {NULL_TRACK_KIND} from '../null_track';
 
 import {ChromeTasksScrollJankTrack} from './chrome_tasks_scroll_jank_track';
 import {addLatencyTracks, EventLatencyTrack} from './event_latency_track';
@@ -36,8 +44,6 @@
   defaultValue: false,
 });
 
-export const INPUT_LATENCY_TRACK = 'InputLatency::';
-
 export const ENABLE_SCROLL_JANK_PLUGIN_V2 = featureFlags.register({
   id: 'enableScrollJankPluginV2',
   name: 'Enable Scroll Jank plugin V2',
@@ -45,10 +51,16 @@
   defaultValue: false,
 });
 
+export const SCROLL_JANK_GROUP_ID = 'chrome-scroll-jank-track-group';
+
 export type ScrollJankTracks = {
   tracksToAdd: AddTrackArgs[],
 };
 
+export type ScrollJankTrackGroup = {
+  tracks: ScrollJankTracks; addTrackGroup: DeferredAction
+}
+
 export interface ScrollJankTrackSpec {
   id: string;
   sqlTableName: string;
@@ -95,36 +107,48 @@
 }
 
 export async function getScrollJankTracks(engine: Engine):
-    Promise<ScrollJankTracks> {
+    Promise<ScrollJankTrackGroup> {
   const result: ScrollJankTracks = {
     tracksToAdd: [],
   };
 
+  const summaryTrackId = uuidv4();
+
+  result.tracksToAdd.push({
+    engineId: engine.id,
+    kind: NULL_TRACK_KIND,
+    trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
+    name: ``,
+    trackGroup: undefined,
+    config: {},
+    id: summaryTrackId,
+  });
+
   const scrolls = addTopLevelScrollTrack(engine);
-  const scrollsResult = await scrolls;
-  let originalLength = result.tracksToAdd.length;
-  result.tracksToAdd.length += scrollsResult.tracksToAdd.length;
-  for (let i = 0; i < scrollsResult.tracksToAdd.length; ++i) {
-    result.tracksToAdd[i + originalLength] = scrollsResult.tracksToAdd[i];
+  for (const scroll of (await scrolls).tracksToAdd) {
+    result.tracksToAdd.push(scroll);
   }
 
   const janks = addScrollJankV3ScrollTrack(engine);
-  const janksResult = await janks;
-  originalLength = result.tracksToAdd.length;
-  result.tracksToAdd.length += janksResult.tracksToAdd.length;
-  for (let i = 0; i < janksResult.tracksToAdd.length; ++i) {
-    result.tracksToAdd[i + originalLength] = janksResult.tracksToAdd[i];
+  for (const jank of (await janks).tracksToAdd) {
+    result.tracksToAdd.push(jank);
   }
 
-  originalLength = result.tracksToAdd.length;
   const eventLatencies = addLatencyTracks(engine);
-  const eventLatencyResult = await eventLatencies;
-  result.tracksToAdd.length += eventLatencyResult.tracksToAdd.length;
-  for (let i = 0; i < eventLatencyResult.tracksToAdd.length; ++i) {
-    result.tracksToAdd[i + originalLength] = eventLatencyResult.tracksToAdd[i];
+  for (const eventLatency of (await eventLatencies).tracksToAdd) {
+    result.tracksToAdd.push(eventLatency);
   }
 
-  return result;
+  const addTrackGroup = Actions.addTrackGroup({
+    engineId: engine.id,
+    name: 'Chrome Scroll Jank',
+    id: SCROLL_JANK_GROUP_ID,
+    collapsed: false,
+    summaryTrackId,
+    fixedOrdering: true,
+  });
+
+  return {tracks: result, addTrackGroup};
 }
 
 class ChromeScrollJankPlugin implements Plugin {
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
index f1db56e..df2556c 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -18,9 +18,6 @@
   getColorForSlice,
 } from '../../common/colorizer';
 import {Engine} from '../../common/engine';
-import {
-  SCROLLING_TRACK_GROUP,
-} from '../../common/state';
 import {globals} from '../../frontend/globals';
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {NewTrackArgs, TrackBase} from '../../frontend/track';
@@ -33,6 +30,7 @@
 
 import {EventLatencyTrackTypes} from './event_latency_track';
 import {
+  SCROLL_JANK_GROUP_ID,
   ScrollJankPluginState,
   ScrollJankTracks as DecideTracksResult,
 } from './index';
@@ -143,7 +141,7 @@
     trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
     name: 'Chrome Scroll Janks',
     config: {},
-    trackGroup: SCROLLING_TRACK_GROUP,
+    trackGroup: SCROLL_JANK_GROUP_ID,
   });
 
   return result;
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
index 97ccc4a..fcfa639 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
@@ -15,7 +15,7 @@
 import {v4 as uuidv4} from 'uuid';
 
 import {Engine} from '../../common/engine';
-import {SCROLLING_TRACK_GROUP} from '../../common/state';
+
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {NewTrackArgs, TrackBase} from '../../frontend/track';
 import {PrimaryTrackSortKey} from '../../public';
@@ -24,8 +24,8 @@
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../custom_sql_table_slices';
-
 import {
+  SCROLL_JANK_GROUP_ID,
   ScrollJankPluginState,
   ScrollJankTracks as DecideTracksResult,
 } from './index';
@@ -94,7 +94,7 @@
     trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
     name: 'Chrome Scrolls',
     config: {},
-    trackGroup: SCROLLING_TRACK_GROUP,
+    trackGroup: SCROLL_JANK_GROUP_ID,
   });
 
   return result;