ui: optimize BreakdownTracks and AndroidBinderViz performance

  Significantly reduce the time it takes to load traces with large breakdown
  track hierarchies (like Android Binder) through three optimizations:

   1. Projected Path for BreakdownTracks: Instead of running the expensive
     intervals_overlap_count macro O(N) times (once per track node), pre-
     calculate all intersections upfront into a shared temporary table.
     Individual tracks now just run a cheap filter/aggregation against this
     table. This is enabled for standard count-based breakdown tracks without
     pivots or complex joins.
   2. Batch SQL Table Creation: On the legacy path, CREATE PERFETTO TABLE
     statements for counter tracks are now accumulated during the hierarchy
     walk and flushed to the engine in a single batched query, reducing IPC
     overhead.
   3. Parallelize Binder Tracks: The AndroidBinderViz plugin now uses
     Promise.all to kick off the generation of both server and client track
     hierarchies concurrently rather than sequentially.
diff --git a/ui/src/components/tracks/breakdown_tracks.ts b/ui/src/components/tracks/breakdown_tracks.ts
index 38f2e44..3b160b8 100644
--- a/ui/src/components/tracks/breakdown_tracks.ts
+++ b/ui/src/components/tracks/breakdown_tracks.ts
@@ -171,17 +171,47 @@
   private modulesClause: string;
   private sliceJoinClause?: string;
   private pivotJoinClause?: string;
+  // Counter tracks defer their CREATE PERFETTO TABLE here on the legacy
+  // (non-projected) path; flushed in one batched engine.query at end of
+  // createTracks(). Unused on the projected path.
+  private pendingTableCreates: string[] = [];
+
+  // Projected-path state. When useProjectedPath is true, createTracks builds
+  // one (id, ts, dur, k0..kN, s0..sN) projection plus one self-intersect
+  // segments table, and per-track renderers query those instead of running
+  // intervals_overlap_count! per node. Eliminates the O(N_tracks) macro
+  // multiplier that dominates trace load for big hierarchies.
+  private readonly useProjectedPath: boolean;
+  private projectedTableName?: string;
+  private segmentsTableName?: string;
+  private projectedAggColNames?: readonly string[];
+  private projectedSliceColNames?: readonly string[];
 
   constructor(props: BreakdownTrackProps) {
     this.props = props;
     this.uri = `/breakdown_tracks_${this.props.aggregation.tableName}`;
+    this.useProjectedPath = BreakdownTracks.shouldUseProjectedPath(props);
 
     this.modulesClause = props.modules
       ? props.modules.map((m) => `INCLUDE PERFETTO MODULE ${m};`).join('\n')
       : '';
 
     if (this.props.aggregationType === BreakdownTrackAggType.COUNT) {
-      this.modulesClause += `\nINCLUDE PERFETTO MODULE intervals.overlap;`;
+      this.modulesClause += this.useProjectedPath
+        ? `\nINCLUDE PERFETTO MODULE intervals.intersect;`
+        : `\nINCLUDE PERFETTO MODULE intervals.overlap;`;
+    }
+
+    if (this.useProjectedPath) {
+      const unique = uuidv4().replace(/-/g, '_');
+      this.projectedTableName = `_breakdown_projected_${unique}`;
+      this.segmentsTableName = `_breakdown_segments_${unique}`;
+      this.projectedAggColNames = this.props.aggregation.columns.map(
+        (_, i) => `k${i}`,
+      );
+      this.projectedSliceColNames = (this.props.slice?.columns ?? []).map(
+        (_, i) => `s${i}`,
+      );
     }
 
     if (this.props.slice?.joins !== undefined) {
@@ -193,7 +223,33 @@
     }
   }
 
+  private static shouldUseProjectedPath(props: BreakdownTrackProps): boolean {
+    return (
+      props.aggregationType === BreakdownTrackAggType.COUNT &&
+      props.aggregation.joins === undefined &&
+      props.slice?.joins === undefined &&
+      props.pivots === undefined
+    );
+  }
+
   private getAggregationQuery(filtersClause: string) {
+    if (this.useProjectedPath) {
+      // Query the shared self-intersect segments table. One row per
+      // (atomic segment, original interval) pair, plus an end-marker row at
+      // each interval's end. SUM(IIF(NOT interval_ends_at_ts, 1, 0)) per
+      // group_id counts active intervals at each segment; end-markers
+      // contribute 0 so the counter naturally drops on segment boundaries
+      // where intervals end.
+      return `
+        SELECT MIN(ts) AS ts,
+               SUM(IIF(interval_ends_at_ts = FALSE, 1, 0)) AS value
+        FROM ${this.segmentsTableName}
+        ${filtersClause}
+        GROUP BY group_id
+        ORDER BY ts
+      `;
+    }
+
     if (this.props.aggregationType === BreakdownTrackAggType.COUNT) {
       return `
         intervals_overlap_count
@@ -251,12 +307,58 @@
       .join('\n');
   }
 
+  // Build the two tables that drive the projected path:
+  //   _breakdown_projected: (id, ts, dur, k0..kN, s0..sN) — one row per source
+  //     row, with the user-supplied agg/slice column expressions pre-evaluated
+  //     under stable names. Filters everywhere downstream become raw column
+  //     equality on this table.
+  //   _breakdown_segments: per-atomic-segment, per-active-id rows from
+  //     interval_self_intersect, denormalized with the agg cols inline so
+  //     per-track counter renderers don't need to JOIN back.
+  private async buildProjectedTables() {
+    const {tsCol, durCol, tableName} = this.props.aggregation;
+    const aggCols = this.props.aggregation.columns;
+    const sliceCols = this.props.slice?.columns ?? [];
+    const idExpr = this.props.sliceIdColumn ?? 'ROW_NUMBER() OVER ()';
+
+    const projectedSelect = [
+      `${idExpr} AS id`,
+      `${tsCol} AS ts`,
+      `${durCol} AS dur`,
+      ...aggCols.map((col, i) => `${col} AS k${i}`),
+      ...sliceCols.map((col, i) => `${col} AS s${i}`),
+    ].join(', ');
+
+    const denormAggCols = this.projectedAggColNames!.map((n) => `p.${n}`).join(
+      ', ',
+    );
+
+    await this.props.trace.engine.query(`
+      CREATE PERFETTO TABLE ${this.projectedTableName} AS
+      SELECT ${projectedSelect}
+      FROM ${tableName};
+
+      CREATE PERFETTO TABLE ${this.segmentsTableName} AS
+      SELECT
+        iss.ts,
+        iss.group_id,
+        iss.interval_ends_at_ts,
+        ${denormAggCols}
+      FROM interval_self_intersect!((
+        SELECT id, ts, dur FROM ${this.projectedTableName}
+      )) iss
+      JOIN ${this.projectedTableName} p USING(id);
+    `);
+  }
+
   async createTracks() {
     if (this.modulesClause !== '') {
       await this.props.trace.engine.query(this.modulesClause);
     }
 
-    if (this.props.aggregationType !== BreakdownTrackAggType.COUNT) {
+    if (this.useProjectedPath) {
+      await this.buildProjectedTables();
+    } else if (this.props.aggregationType !== BreakdownTrackAggType.COUNT) {
       await this.props.trace.engine.query(`
         CREATE OR REPLACE PERFETTO FUNCTION _ui_dev_perfetto_breakdown_tracks_is_spans_overlapping(
           ts1 LONG,
@@ -276,8 +378,16 @@
       [],
     );
 
-    const aggColNames = this.props.aggregation.columns;
-    const sliceColNames = this.props.slice?.columns ?? [];
+    // Column names used by the hierarchy walk to read group-by results and
+    // build per-track filters. On the projected path these are the stable
+    // projected names (k0..kN, s0..sN); on the legacy path they are the
+    // original column expressions.
+    const aggColNames = this.useProjectedPath
+      ? [...this.projectedAggColNames!]
+      : this.props.aggregation.columns;
+    const sliceColNames = this.useProjectedPath
+      ? [...this.projectedSliceColNames!]
+      : this.props.slice?.columns ?? [];
     const pivotColNames = this.props.pivots?.columns ?? [];
 
     const allColNames = [];
@@ -285,13 +395,19 @@
     allColNames.push(...sliceColNames);
     allColNames.push(...pivotColNames);
 
-    const query = `
-      SELECT ${allColNames.join(', ')}
-      FROM ${this.props.aggregation.tableName}
-      ${this.sliceJoinClause ?? ''}
-      ${this.pivotJoinClause ?? ''}
-      GROUP BY ${allColNames.join(', ')}
-    `;
+    const query = this.useProjectedPath
+      ? `
+        SELECT ${allColNames.join(', ')}
+        FROM ${this.projectedTableName}
+        GROUP BY ${allColNames.join(', ')}
+      `
+      : `
+        SELECT ${allColNames.join(', ')}
+        FROM ${this.props.aggregation.tableName}
+        ${this.sliceJoinClause ?? ''}
+        ${this.pivotJoinClause ?? ''}
+        GROUP BY ${allColNames.join(', ')}
+      `;
     const res = await this.props.trace.engine.query(query);
 
     await this.createBreakdownHierarchy(
@@ -302,9 +418,18 @@
       pivotColNames,
     );
 
+    await this.flushPendingTableCreates();
+
     return rootTrackNode;
   }
 
+  private async flushPendingTableCreates() {
+    if (this.pendingTableCreates.length === 0) return;
+    const batch = this.pendingTableCreates.join('\n');
+    this.pendingTableCreates = [];
+    await this.props.trace.engine.query(batch);
+  }
+
   private async createBreakdownHierarchy(
     rootNode: TrackNode,
     queryResult: QueryResult,
@@ -422,7 +547,29 @@
     return await this.createTrackNode(
       title,
       newFilters,
-      async (uri: string, filtersClause: string) => {
+      (uri: string, filtersClause: string) => {
+        // On the projected path, source from the pre-computed projection:
+        // id/ts/dur are already aliased there, and the slice column is
+        // stored under sN, so the per-track query is a cheap raw-eq filter.
+        // shouldUseProjectedPath excludes joins+pivots, so trackType is
+        // always SLICE here.
+        const src = this.useProjectedPath
+          ? `
+              SELECT id, ts, dur,
+                     ${this.projectedSliceColNames![columnIndex]} AS name
+              FROM ${this.projectedTableName}
+              ${filtersClause}
+            `
+          : `
+              SELECT
+                ${this.props.sliceIdColumn ? this.props.sliceIdColumn : 'ROW_NUMBER() OVER()'} AS id,
+                ${sqlInfo.tsCol} AS ts,
+                ${sqlInfo.durCol} AS dur,
+                ${sqlInfo.columns[columnIndex]} AS name
+              FROM ${this.props.aggregation.tableName}
+              ${joinClause}
+              ${filtersClause}
+            `;
         return SliceTrack.create({
           trace: this.props.trace,
           uri,
@@ -433,16 +580,7 @@
               dur: LONG,
               name: STR,
             },
-            src: `
-              SELECT
-                ${this.props.sliceIdColumn ? this.props.sliceIdColumn : 'ROW_NUMBER() OVER()'} AS id,
-                ${sqlInfo.tsCol} AS ts,
-                ${sqlInfo.durCol} AS dur,
-                ${sqlInfo.columns[columnIndex]} AS name
-              FROM ${this.props.aggregation.tableName}
-              ${joinClause}
-              ${filtersClause}
-            `,
+            src,
           }),
           detailsPanel: this.props.detailsPanel
             ? () => this.props.detailsPanel!(this.props.trace)
@@ -466,13 +604,30 @@
       name,
       newFilters,
       (uri: string, filtersClause: string) => {
-        return CounterTrack.createMaterialized({
+        const sqlSource = `SELECT ts, value FROM (${this.getAggregationQuery(filtersClause)})`;
+        if (this.useProjectedPath) {
+          // Lazy: the per-render macro work that would dominate trace load
+          // never happens here — getAggregationQuery returns a cheap filter
+          // over the shared self-intersect segments table, and CounterTrack's
+          // useData fires it only on render.
+          return CounterTrack.create({
+            trace: this.props.trace,
+            uri,
+            sqlSource,
+          });
+        }
+        // Legacy: defer the CREATE PERFETTO TABLE so all counter tables in
+        // this BreakdownTracks instance flush in one batched engine.query at
+        // end of createTracks(). The renderer holds the chosen table name;
+        // the table is materialized before createTracks() returns.
+        const tableName = `_breakdown_counter_${uuidv4().replace(/-/g, '_')}`;
+        this.pendingTableCreates.push(
+          `CREATE PERFETTO TABLE ${tableName} AS ${sqlSource};`,
+        );
+        return CounterTrack.create({
           trace: this.props.trace,
           uri,
-          sqlSource: `
-            SELECT ts, value
-            FROM (${this.getAggregationQuery(filtersClause)})
-          `,
+          sqlSource: tableName,
         });
       },
       (filterClause) => this.getCounterTrackSortOrder(filterClause),
@@ -482,14 +637,14 @@
   private async createTrackNode(
     name: string,
     filters: Filter[],
-    createTrack: (uri: string, filtersClause: string) => Promise<TrackRenderer>,
+    createTrack: (uri: string, filtersClause: string) => TrackRenderer,
     getSortOrder?: (filterClause: string) => Promise<number>,
   ) {
     const filtersClause =
       filters.length > 0 ? `\nWHERE ${buildFilterSqlClause(filters)}` : '';
     const uri = `${this.uri}_${uuidv4()}`;
 
-    const renderer = await createTrack(uri, filtersClause);
+    const renderer = createTrack(uri, filtersClause);
 
     this.props.trace.tracks.registerTrack({
       uri,
diff --git a/ui/src/plugins/com.android.AndroidBinderViz/index.ts b/ui/src/plugins/com.android.AndroidBinderViz/index.ts
index 04ff624..eaa47e5 100644
--- a/ui/src/plugins/com.android.AndroidBinderViz/index.ts
+++ b/ui/src/plugins/com.android.AndroidBinderViz/index.ts
@@ -18,24 +18,29 @@
 } from '../../components/tracks/breakdown_tracks';
 import type {PerfettoPlugin} from '../../public/plugin';
 import type {Trace} from '../../public/trace';
+import type {TrackNode} from '../../public/workspace';
 import {BinderSliceDetailsPanel} from './details_panel';
 
 export default class implements PerfettoPlugin {
   static readonly id = 'com.android.AndroidBinderViz';
 
   async onTraceLoad(ctx: Trace): Promise<void> {
-    await this.createBinderTransactionTrack(
-      ctx,
-      'server',
-      'client',
-      'binder_txn_id',
-    );
-    await this.createBinderTransactionTrack(
-      ctx,
-      'client',
-      'server',
-      'binder_reply_id',
-    );
+    const [serverRoot, clientRoot] = await Promise.all([
+      this.createBinderTransactionTrack(
+        ctx,
+        'server',
+        'client',
+        'binder_txn_id',
+      ),
+      this.createBinderTransactionTrack(
+        ctx,
+        'client',
+        'server',
+        'binder_reply_id',
+      ),
+    ]);
+    ctx.defaultWorkspace.addChildInOrder(serverRoot);
+    ctx.defaultWorkspace.addChildInOrder(clientRoot);
   }
 
   async createBinderTransactionTrack(
@@ -43,7 +48,7 @@
     perspective: string,
     oppositePerspective: string,
     sliceIdColumn?: string,
-  ) {
+  ): Promise<TrackNode> {
     const binderCounterBreakdowns = new BreakdownTracks({
       trace: ctx,
       trackTitle: `Binder ${perspective} Transaction Counts`,
@@ -72,8 +77,6 @@
       detailsPanel: (trace: Trace) => new BinderSliceDetailsPanel(trace),
     });
 
-    ctx.defaultWorkspace.addChildInOrder(
-      await binderCounterBreakdowns.createTracks(),
-    );
+    return await binderCounterBreakdowns.createTracks();
   }
 }