stdlib: Add interval_self_intersect_by_group macro

This enables efficient computation of self-intersecting intervals
within partitions (e.g., per-process) without cross-partition
interference.

Change-Id: I7105c8b06740a7ad381d60e74747301489b74a06
diff --git a/src/trace_processor/perfetto_sql/stdlib/intervals/intersect.sql b/src/trace_processor/perfetto_sql/stdlib/intervals/intersect.sql
index 0dbcde0..09c6835 100644
--- a/src/trace_processor/perfetto_sql/stdlib/intervals/intersect.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/intervals/intersect.sql
@@ -272,3 +272,72 @@
   JOIN _atomic_segments a ON a.ts = e.ts
   WHERE e.is_start = FALSE
 );
+
+-- Like interval_self_intersect but partitions by group_column. Intervals in
+-- different groups do not intersect with each other.
+--
+-- Runtime is O(n log n + m) per group, where n is the number of intervals in
+-- each group and m is the size of the output.
+CREATE PERFETTO MACRO interval_self_intersect_by_group(
+  -- Table or subquery containing interval data.
+  intervals TableOrSubquery,
+  -- Column containing group name for partitioning.
+  group_column ColumnName
+)
+RETURNS TableOrSubquery
+AS
+(
+  WITH
+    _all_endpoints AS (
+      SELECT id, ts, TRUE as is_start, $group_column AS group_name FROM $intervals
+      UNION
+      SELECT id, ts + dur AS ts, FALSE as is_start, $group_column AS group_name FROM $intervals
+    ),
+    _atomic_segments AS (
+      SELECT
+        ROW_NUMBER() OVER (ORDER BY group_name, ts) AS id,
+        ts,
+        IFNULL(LEAD(ts) OVER (PARTITION BY group_name ORDER BY ts) - ts, 0) AS dur,
+        group_name
+      FROM _all_endpoints
+      GROUP BY ts, group_name
+    ),
+    _ii AS (
+      SELECT
+        ii.ts,
+        ii.dur,
+        ii.id_0 AS group_id,
+        ii.id_1 AS original_id,
+        ii.group_name
+      FROM _interval_intersect!((_atomic_segments, $intervals), (group_name)) ii
+    ),
+    _original_ends AS (
+      SELECT id, ts + dur AS end_ts, $group_column AS group_name FROM $intervals
+    )
+  -- Part A: Standard segments
+  SELECT
+    ts,
+    dur,
+    group_id,
+    original_id AS id,
+    FALSE AS interval_ends_at_ts,
+    group_name
+  FROM _ii
+  WHERE dur > 0
+
+  UNION ALL
+
+  -- Part B: End markers.
+  -- We join back to _atomic_segments to get the 'next' duration
+  -- to match the original implementation's quirk.
+  SELECT
+    e.ts AS ts,
+    a.dur AS dur,
+    a.id AS group_id,
+    e.id AS id,
+    TRUE AS interval_ends_at_ts,
+    e.group_name
+  FROM _all_endpoints e
+  JOIN _atomic_segments a ON a.ts = e.ts AND a.group_name = e.group_name
+  WHERE e.is_start = FALSE
+);
diff --git a/test/trace_processor/diff_tests/stdlib/intervals/tests.py b/test/trace_processor/diff_tests/stdlib/intervals/tests.py
index 5ca0304..2d8d2d2 100644
--- a/test/trace_processor/diff_tests/stdlib/intervals/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/intervals/tests.py
@@ -305,6 +305,52 @@
         310,0,11,5,1
         """))
 
+  def test_intersect_list_by_group(self):
+    return DiffTestBlueprint(
+        trace=TextProto(""),
+        query="""
+        INCLUDE PERFETTO MODULE intervals.intersect;
+
+        WITH
+          data(ts, dur, id, group_name) AS (
+            VALUES
+              -- Group A: overlapping intervals
+              (10, 100, 0, 'A'),
+              (20, 40, 1, 'A'),
+              (30, 120, 2, 'A'),
+              -- Group B: separate intervals
+              (200, 10, 3, 'B'),
+              (200, 20, 4, 'B'),
+              -- Group C: single interval
+              (300, 10, 5, 'C')
+          )
+        SELECT *
+        FROM interval_self_intersect_by_group!(data, group_name)
+        ORDER BY group_name ASC, ts ASC, id ASC;
+        """,
+        out=Csv("""
+        "ts","dur","group_id","id","interval_ends_at_ts","group_name"
+        10,10,1,0,0,"A"
+        20,10,2,0,0,"A"
+        20,10,2,1,0,"A"
+        30,30,3,0,0,"A"
+        30,30,3,1,0,"A"
+        30,30,3,2,0,"A"
+        60,50,4,0,0,"A"
+        60,50,4,1,1,"A"
+        60,50,4,2,0,"A"
+        110,40,5,0,1,"A"
+        110,40,5,2,0,"A"
+        150,0,6,2,1,"A"
+        200,10,7,3,0,"B"
+        200,10,7,4,0,"B"
+        210,10,8,3,1,"B"
+        210,10,8,4,0,"B"
+        220,0,9,4,1,"B"
+        300,10,10,5,0,"C"
+        310,0,11,5,1,"C"
+        """))
+
   def test_interval_merge_overlapping_partitioned(self):
     return DiffTestBlueprint(
         trace=TextProto(""),