-- -- Copyright 2023 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 -- -- https://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. INCLUDE PERFETTO MODULE intervals.intersect; -- Compute the distribution of the overlap of the given intervals over time. -- -- Each interval is a (ts, dur) pair and the overlap represented as a (ts, value) -- counter, with the value corresponding to the number of intervals that overlap -- the given timestamp and interval until the next timestamp. CREATE PERFETTO MACRO intervals_overlap_count( -- Table or subquery containing interval data. segments TableOrSubquery, -- Column containing interval starts (usually `ts`). ts_column ColumnName, -- Column containing interval durations (usually `dur`). dur_column ColumnName ) -- The returned table has the schema (ts TIMESTAMP, value LONG). -- |ts| is the timestamp when the number of open segments changed. |value| is -- the number of open segments. RETURNS TableOrSubquery AS ( -- Algorithm: for each segment, emit a +1 at the start and a -1 at the end. -- Then, merge events with the same timestamp and compute a cumulative sum. WITH _starts AS ( SELECT 1 AS delta, $ts_column AS ts FROM $segments ), _ends AS ( SELECT -1 AS delta, $ts_column + $dur_column AS ts FROM $segments WHERE $dur_column != -1 ), _events AS ( SELECT * FROM _starts UNION ALL SELECT * FROM _ends ), -- Merge events with the same timestamp to avoid artifacts in the data. _merged_events AS ( SELECT ts, sum(delta) AS delta FROM _events GROUP BY ts ) SELECT ts, sum(delta) OVER (ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS value FROM _merged_events ORDER BY ts ); -- Compute the distribution of the overlap of the given intervals over time from -- slices in a same group. -- -- Each interval is a (ts, dur, group) triple and the overlap represented as a -- (ts, value, group) counter, with the value corresponding to the number of -- intervals that belong to the same group and overlap the given timestamp and -- interval until the next timestamp. CREATE PERFETTO MACRO intervals_overlap_count_by_group( -- Table or subquery containing interval data. segments TableOrSubquery, -- Column containing interval starts (usually `ts`). ts_column ColumnName, -- Column containing interval durations (usually `dur`). dur_column ColumnName, -- Column containing group name for grouping. group_column ColumnName ) -- The returned table has the schema (ts INT64, value UINT32, group_name) where -- the type of group_name is the same as that in |segments|. -- |ts| is the timestamp when the number of open segments changed. |value| is -- the number of open segments. |group_name| is the name of a group used for the -- overlap calculation. RETURNS TableOrSubquery AS ( -- Algorithm: for each segment, emit a +1 at the start and a -1 at the end. -- Then, merge events with the same timestamp and compute a cumulative sum for -- each group. WITH _starts AS ( SELECT 1 AS delta, $ts_column AS ts, $group_column AS group_name FROM $segments ), _ends AS ( SELECT -1 AS delta, $ts_column + $dur_column AS ts, $group_column AS group_name FROM $segments WHERE $dur_column != -1 ), _events AS ( SELECT * FROM _starts UNION ALL SELECT * FROM _ends ), -- Merge events with the same timestamp to avoid artifacts in the data. _merged_events AS ( SELECT ts, sum(delta) AS delta, group_name FROM _events GROUP BY ts, group_name ) SELECT ts, sum(delta) OVER (PARTITION BY group_name ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS value, group_name FROM _merged_events ORDER BY ts ); -- Returns whether |intervals| contains any overlapping intervals. Useful for -- checking if provided table/subquery can be used for intervals_intersect -- macro. CREATE PERFETTO MACRO _intervals_overlap_in_table( -- Table/subquery of intervals with |ts| and |dur| columns. intervals TableOrSubquery ) -- Returns 1 if table contains overlapping intervals. Otherwise returns 0. RETURNS Expr AS ( WITH ts_with_next AS ( SELECT ts + dur AS ts_end, -- The last slice will have |next_ts == NULL|, but it's not an issue as if -- it's the last slice we know that it will not overlap with the next one. lead(ts) OVER (ORDER BY ts) AS next_ts FROM $intervals WHERE dur != -1 ), filtered AS ( SELECT * FROM ts_with_next WHERE ts_end > next_ts LIMIT 1 ) SELECT count() AS has_overlaps FROM filtered ); -- Merges a |roots_table| and |children_table| into one table. See _intervals_flatten -- that accepts the output of this macro to flatten intervals. -- See: _intervals_merge_root_and_children_by_intersection. CREATE PERFETTO MACRO _intervals_merge_root_and_children( -- Table or subquery containing all the root intervals: (id, ts, dur). -- Note that parent_id is not necessary in this table as it will be NULL anyways. roots_table TableOrSubquery, -- Table or subquery containing all the child intervals: -- (root_id, id, parent_id, ts, dur) children_table TableOrSubquery ) -- The returned table has the schema (root_id LONG, root_ts TIMESTAMP, root_dur, LONG, -- id LONG, parent_id LONG, ts TIMESTAMP, dur LONG). RETURNS TableOrSubquery AS ( WITH _roots AS ( SELECT id AS root_id, ts AS root_ts, dur AS root_dur FROM ( $roots_table ) WHERE dur > 0 ), _children AS ( SELECT * FROM ( $children_table ) WHERE dur > 0 ), _roots_without_children AS ( SELECT root_id FROM _roots EXCEPT SELECT DISTINCT parent_id AS root_id FROM _children ) SELECT _roots.root_id, _roots.root_ts, _roots.root_dur, _children.id, _children.parent_id, _children.ts, _children.dur FROM _children JOIN _roots USING (root_id) UNION ALL -- Handle singleton roots SELECT root_id, root_ts, root_dur, NULL AS id, NULL AS parent_id, NULL AS ts, NULL AS dur FROM _roots_without_children JOIN _roots USING (root_id) ); -- Merges a |roots_table| and |children_table| into one table. See _intervals_flatten -- that accepts the output of this macro to flatten intervals. -- This is very similar to _intervals_merge_root_and_children but there is no explicit -- root_id shared between the root and the children. Instead an _interval_intersect is -- used to derive the root and child relationships. CREATE PERFETTO MACRO _intervals_merge_root_and_children_by_intersection( -- Table or subquery containing all the root intervals: (id, ts, dur). -- Note that parent_id is not necessary in this table as it will be NULL anyways. roots_table TableOrSubquery, -- Table or subquery containing all the child intervals: -- (root_id, id, parent_id, ts, dur) children_table TableOrSubquery, -- intersection key used in deriving the root child relationships. key ColumnName ) RETURNS TableOrSubQuery AS ( WITH _roots AS ( SELECT * FROM $roots_table WHERE dur > 0 ORDER BY ts ), _children AS ( SELECT * FROM $children_table WHERE dur > 0 ORDER BY ts ) SELECT ii.ts, ii.dur, _children.id, iif(_children.parent_id IS NULL, id_1, _children.parent_id) AS parent_id, _roots.id AS root_id, _roots.ts AS root_ts, _roots.dur AS root_dur, ii.$key FROM _interval_intersect!((_children, _roots), ($key)) AS ii JOIN _children ON _children.id = id_0 JOIN _roots ON _roots.id = id_1 ); -- Partition and flatten a hierarchy of intervals into non-overlapping intervals where -- each resulting interval is the leaf in the hierarchy at any given time. The result also -- denotes the 'self-time' of each interval. -- -- Each interval is a (root_id, root_ts, root_dur, id, parent_id, ts, dur) and the overlap is -- represented as a (root_id, id, parent_id, ts, dur). -- Note that, children intervals must not be longer than any ancestor interval. -- See _intervals_merge_root_and_children that can be used to generate input to this macro -- from two different root and children tables. CREATE PERFETTO MACRO _intervals_flatten( children_with_roots_table TableOrSubquery ) -- The returned table has the schema (root_id LONG, id LONG, ts TIMESTAMP, dur LONG). RETURNS TableOrSubquery AS ( -- Algorithm: Sort all the start and end timestamps of the children within a root. -- The interval duration between one timestamp and the next is one result. -- If the timestamp is a start, the id is the id of the interval, if it's an end, -- it's the parent_id. -- Special case the edges of the roots and roots without children. WITH _children_with_roots AS ( SELECT * FROM ( $children_with_roots_table ) WHERE root_dur > 0 AND ( dur IS NULL OR dur > 0 ) ), _ends AS ( SELECT root_id, root_ts, root_dur, coalesce(parent_id, root_id) AS id, ts + dur AS ts FROM _children_with_roots WHERE id IS NOT NULL ), _events AS ( SELECT root_id, root_ts, root_dur, id, ts, 1 AS priority FROM _children_with_roots UNION ALL SELECT root_id, root_ts, root_dur, id, ts, 0 AS priority FROM _ends ), _events_deduped AS ( SELECT root_id, root_ts, root_dur, id, ts FROM _events GROUP BY root_id, ts HAVING priority = max(priority) ), _intervals AS ( SELECT root_id, root_ts, root_dur, id, ts, lead(ts) OVER (PARTITION BY root_id ORDER BY ts) - ts AS dur FROM _events_deduped ), _only_middle AS ( SELECT * FROM _intervals WHERE dur > 0 ), _only_start AS ( SELECT root_id, root_id AS id, root_ts AS ts, min(ts) - root_ts AS dur FROM _only_middle GROUP BY root_id HAVING dur > 0 ), _only_end AS ( SELECT root_id, root_id AS id, max(ts + dur) AS ts, root_ts + root_dur - max(ts + dur) AS dur FROM _only_middle GROUP BY root_id HAVING dur > 0 ), _only_singleton AS ( SELECT root_id, root_id AS id, root_ts AS ts, root_dur AS dur FROM _children_with_roots WHERE id IS NULL GROUP BY root_id ) SELECT root_id, id, ts, dur FROM _only_middle UNION ALL SELECT root_id, id, ts, dur FROM _only_start UNION ALL SELECT root_id, id, ts, dur FROM _only_end UNION ALL SELECT root_id, id, ts, dur FROM _only_singleton ); -- Merge intervals intervals when they overlap to generate a minimum covering set of -- intervals with no overlap. The intervals are closed (contain both endpoints) and -- we consider two intervals overlapping -- (a) the intervals overlap or -- (b) if the end point of one interval is within epsilon of the start point of -- the other CREATE PERFETTO MACRO interval_remove_overlap( -- Table or subquery containing interval data. intervals TableOrSubquery, -- Constant expression for a tolerance in testing overlap (usually `0`) epsilon Expr ) RETURNS TableOrSubquery AS ( -- Algorithm: use intervals_overlap_count to generate a counter track. Pass over -- the counter track from left to right, creating an interval when the counter -- first becomes non-zero and ending an interval when it becomes zero again. WITH _w_prev_count AS ( SELECT ts, value, lag(value, 1, 0) OVER (ORDER BY ts) AS prev_value FROM intervals_overlap_count !($intervals, ts, (dur + $epsilon)) ORDER BY ts ASC ), _end_points AS ( SELECT ts, value FROM _w_prev_count WHERE -- start of merged intervals prev_value = 0 -- end of merged intervals OR value = 0 ), _together AS ( SELECT ts - iif(value = 0, $epsilon, 0) AS ts, value, lag(ts, 1, NULL) OVER (ORDER BY ts) AS prev_ts FROM _end_points ) SELECT prev_ts AS ts, ts - prev_ts AS dur FROM _together WHERE value = 0 );