--
-- 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;

INCLUDE PERFETTO MODULE android.monitor_contention;

INCLUDE PERFETTO MODULE std.metasql.unparenthesize;

-- 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 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_merge_overlapping(
  -- 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
);

-- Merge overlapping intervals within each partition group to generate a minimum
-- covering set of intervals with no overlap within each partition.
--
-- For each partition, this macro merges overlapping intervals into
-- non-overlapping intervals. The result contains intervals where at least
-- one input interval is active.
--
-- For example, with partition 'A':
--   Input: (ts=1, dur=10), (ts=5, dur=12)
--   Output: (ts=1, dur=16)
CREATE PERFETTO MACRO interval_merge_overlapping_partitioned(
  -- Table or subquery containing interval data.
  intervals TableOrSubquery,
  -- Column name for partition grouping.
  partition_columns ColumnNameList
)
-- The returned table has the schema (ts TIMESTAMP, dur DURATION, partitions).
-- |ts| is the start of the merged interval. |dur| is the duration of the
-- merged interval. |partitions| is all of the columns in partition_columns.
RETURNS TableOrSubquery
AS (
  -- Algorithm: For each partition, merge overlaps in three steps:
  -- 1. Find the max endpoint for **preceding** slices in the same partition.
  -- 2. Number groups by counting times when a slices timestamp is greater than
  --    the maximum endpoint so far. This indicates a gap, and thus new group.
  -- 3. Aggreagate slices in the same group to find the start and end time.
  WITH
    _max_endpoint_so_far AS (
      SELECT
        ts,
        dur,
        metasql_unparenthesize_exprlist!($partition_columns),
        max(ts + dur) OVER (PARTITION BY metasql_unparenthesize_exprlist!($partition_columns) ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS max_endpoint_so_far
      FROM $intervals
      WHERE
        dur >= 0
    ),
    _numbered_groups AS (
      SELECT
        *,
        sum(coalesce(ts > max_endpoint_so_far, TRUE)) OVER (PARTITION BY metasql_unparenthesize_exprlist!($partition_columns) ORDER BY ts) AS overlap_group_number
      FROM _max_endpoint_so_far
    )
  SELECT
    min(ts) AS ts,
    max(ts + dur) - min(ts) AS dur,
    metasql_unparenthesize_exprlist!($partition_columns)
  FROM _numbered_groups
  GROUP BY
    metasql_unparenthesize_exprlist!($partition_columns),
    overlap_group_number
);
