--
-- Copyright 2024 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.

-- Computes the "reachable" set of nodes in a directed graph from a given set
-- of starting nodes by performing a depth-first search on the graph. The
-- returned nodes are structured as a tree with parent-child relationships
-- corresponding to the order in which nodes were encountered by the DFS.
--
-- While this macro can be used directly by end users (hence being public),
-- it is primarily intended as a lower-level building block upon which higher
-- level functions/macros in the standard library can be built.
--
-- Example usage on traces containing heap graphs:
-- ```
-- -- Compute the reachable nodes from the first heap root.
-- SELECT *
-- FROM graph_reachable_dfs!(
--   (
--     SELECT
--       owner_id AS source_node_id,
--       owned_id as dest_node_id
--     FROM heap_graph_reference
--     WHERE owned_id IS NOT NULL
--   ),
--   (SELECT id FROM heap_graph_object WHERE root_type IS NOT NULL)
-- );
-- ```
CREATE PERFETTO MACRO graph_reachable_dfs(
  -- A table/view/subquery corresponding to a directed graph on which the
  -- reachability search should be performed. This table must have the columns
  -- "source_node_id" and "dest_node_id" corresponding to the two nodes on
  -- either end of the edges in the graph.
  --
  -- Note: the columns must contain uint32 similar to ids in trace processor
  -- tables (i.e. the values should be relatively dense and close to zero). The
  -- implementation makes assumptions on this for performance reasons and, if
  -- this criteria is not, can lead to enormous amounts of memory being
  -- allocated.
  graph_table TableOrSubquery,
  -- A table/view/subquery corresponding to the list of start nodes for
  -- the BFS. This table must have a single column "node_id".
  start_nodes TableOrSubquery
)
-- The returned table has the schema (node_id LONG, parent_node_id LONG).
-- |node_id| is the id of the node from the input graph and |parent_node_id|
-- is the id of the node which was the first encountered predecessor in a DFS
-- search of the graph.
RETURNS TableOrSubquery
AS (
  -- Rename the generic columns of __intrinsic_table_ptr to the actual columns.
  SELECT
    c0 AS node_id,
    c1 AS parent_node_id
  FROM __intrinsic_table_ptr(
    __intrinsic_dfs(
      (
        SELECT
          __intrinsic_graph_agg(g.source_node_id, g.dest_node_id)
        FROM $graph_table AS g
      ),
      (
        SELECT
          __intrinsic_array_agg(t.node_id) AS arr
        FROM $start_nodes AS t
      )
    )
  )
  -- Bind the dynamic columns in the |__intrinsic_table_ptr| to the columns of
  -- the DFS table.
  WHERE
    __intrinsic_table_ptr_bind(c0, 'node_id')
    AND __intrinsic_table_ptr_bind(c1, 'parent_node_id')
);

-- Computes the "reachable" set of nodes in a directed graph from a given
-- starting node by performing a breadth-first search on the graph. The returned
-- nodes are structured as a tree with parent-child relationships corresponding
-- to the order in which nodes were encountered by the BFS.
--
-- While this macro can be used directly by end users (hence being public),
-- it is primarily intended as a lower-level building block upon which higher
-- level functions/macros in the standard library can be built.
--
-- Example usage on traces containing heap graphs:
-- ```
-- -- Compute the reachable nodes from all heap roots.
-- SELECT *
-- FROM graph_reachable_bfs!(
--   (
--     SELECT
--       owner_id AS source_node_id,
--       owned_id as dest_node_id
--     FROM heap_graph_reference
--     WHERE owned_id IS NOT NULL
--   ),
--   (SELECT id FROM heap_graph_object WHERE root_type IS NOT NULL)
-- );
-- ```
CREATE PERFETTO MACRO graph_reachable_bfs(
  -- A table/view/subquery corresponding to a directed graph on which the
  -- reachability search should be performed. This table must have the columns
  -- "source_node_id" and "dest_node_id" corresponding to the two nodes on
  -- either end of the edges in the graph.
  --
  -- Note: the columns must contain uint32 similar to ids in trace processor
  -- tables (i.e. the values should be relatively dense and close to zero). The
  -- implementation makes assumptions on this for performance reasons and, if
  -- this criteria is not, can lead to enormous amounts of memory being
  -- allocated.
  graph_table TableOrSubquery,
  -- A table/view/subquery corresponding to the list of start nodes for
  -- the BFS. This table must have a single column "node_id".
  start_nodes TableOrSubquery
)
-- The returned table has the schema (node_id LONG, parent_node_id LONG).
-- |node_id| is the id of the node from the input graph and |parent_node_id|
-- is the id of the node which was the first encountered predecessor in a BFS
-- search of the graph.
RETURNS TableOrSubquery
AS (
  -- Rename the generic columns of __intrinsic_table_ptr to the actual columns.
  SELECT
    c0 AS node_id,
    c1 AS parent_node_id
  FROM __intrinsic_table_ptr(
    __intrinsic_bfs(
      (
        SELECT
          __intrinsic_graph_agg(g.source_node_id, g.dest_node_id)
        FROM $graph_table AS g
      ),
      (
        SELECT
          __intrinsic_array_agg(t.node_id) AS arr
        FROM $start_nodes AS t
      )
    )
  )
  -- Bind the dynamic columns in the |__intrinsic_table_ptr| to the columns of
  -- the DFS table.
  WHERE
    __intrinsic_table_ptr_bind(c0, 'node_id')
    AND __intrinsic_table_ptr_bind(c1, 'parent_node_id')
);

-- Computes the next sibling node in a directed graph. The next node under a parent node
-- is determined by on the |sort_key|, which should be unique for every node under a parent.
-- The order of the next sibling is undefined if the |sort_key| is not unique.
--
-- Example usage:
-- ```
-- -- Compute the next sibling:
-- SELECT *
-- FROM graph_next_sibling!(
--   (
--     SELECT
--       id AS node_id,
--       parent_id AS node_parent_id,
--       ts AS sort_key
--     FROM slice
--   )
-- );
-- ```
CREATE PERFETTO MACRO graph_next_sibling(
  -- A table/view/subquery corresponding to a directed graph for which to find the next sibling.
  -- This table must have the columns "node_id", "node_parent_id" and "sort_key".
  graph_table TableOrSubquery
)
-- The returned table has the schema (node_id LONG, next_node_id LONG).
-- |node_id| is the id of the node from the input graph and |next_node_id|
-- is the id of the node which is its next sibling.
RETURNS TableOrSubquery
AS (
  SELECT
    node_id,
    lead(node_id) OVER (PARTITION BY node_parent_id ORDER BY sort_key) AS next_node_id
  FROM $graph_table
);

-- Computes the "reachable" set of nodes in a directed graph from a set of
-- starting (root) nodes by performing a depth-first search from each root node on the graph.
-- The search is bounded by the sum of edge weights on the path and the root node specifies the
-- max weight (inclusive) allowed before stopping the search.
-- The returned nodes are structured as a tree with parent-child relationships corresponding
-- to the order in which nodes were encountered by the DFS. Each row also has the root node from
-- which where the edge was encountered.
--
-- While this macro can be used directly by end users (hence being public),
-- it is primarily intended as a lower-level building block upon which higher
-- level functions/macros in the standard library can be built.
--
-- Example usage on traces with sched info:
-- ```
-- -- Compute the reachable nodes from a sched wakeup chain
-- INCLUDE PERFETTO MODULE sched.thread_executing_spans;
--
-- SELECT *
-- FROM
--   graph_reachable_dfs_bounded
--    !(
--      (
--        SELECT
--          id AS source_node_id,
--          COALESCE(parent_id, id) AS dest_node_id,
--          id - COALESCE(parent_id, id) AS edge_weight
--        FROM _wakeup_chain
--      ),
--      (
--        SELECT
--          id AS root_node_id,
--          id - COALESCE(prev_id, id) AS root_target_weight
--        FROM _wakeup_chain
--      ));
-- ```
CREATE PERFETTO MACRO graph_reachable_weight_bounded_dfs(
  -- A table/view/subquery corresponding to a directed graph on which the
  -- reachability search should be performed. This table must have the columns
  -- "source_node_id" and "dest_node_id" corresponding to the two nodes on
  -- either end of the edges in the graph and an "edge_weight" corresponding to the
  -- weight of the edge between the node.
  --
  -- Note: the columns must contain uint32 similar to ids in trace processor
  -- tables (i.e. the values should be relatively dense and close to zero). The
  -- implementation makes assumptions on this for performance reasons and, if
  -- this criteria is not, can lead to enormous amounts of memory being
  -- allocated.
  graph_table TableOrSubquery,
  -- A table/view/subquery corresponding to start nodes to |graph_table| which will be the
  -- roots of the reachability trees. This table must have the columns
  -- "root_node_id" and "root_target_weight" corresponding to the starting node id and the max
  -- weight allowed on the tree.
  --
  -- Note: the columns must contain uint32 similar to ids in trace processor
  -- tables (i.e. the values should be relatively dense and close to zero). The
  -- implementation makes assumptions on this for performance reasons and, if
  -- this criteria is not, can lead to enormous amounts of memory being
  -- allocated.
  root_table TableOrSubquery,
  -- Whether the target_weight is a floor weight or ceiling weight.
  -- If it's floor, the search stops right after we exceed the target weight, and we
  -- include the node that pushed just passed the target. If ceiling, the search stops
  -- right before the target weight and the node that would have pushed us passed the
  -- target is not included.
  is_target_weight_floor Expr
)
-- The returned table has the schema (root_node_id, node_id LONG, parent_node_id LONG).
-- |root_node_id| is the id of the starting node under which this edge was encountered.
-- |node_id| is the id of the node from the input graph and |parent_node_id|
-- is the id of the node which was the first encountered predecessor in a DFS
-- search of the graph.
RETURNS TableOrSubquery
AS (
  WITH
    __temp_graph_table AS (
      SELECT
        *
      FROM $graph_table
    ),
    __temp_root_table AS (
      SELECT
        *
      FROM $root_table
    )
  SELECT
    dt.root_node_id,
    dt.node_id,
    dt.parent_node_id
  FROM __intrinsic_dfs_weight_bounded(
    (
      SELECT
        repeatedfield(source_node_id)
      FROM __temp_graph_table
    ),
    (
      SELECT
        repeatedfield(dest_node_id)
      FROM __temp_graph_table
    ),
    (
      SELECT
        repeatedfield(edge_weight)
      FROM __temp_graph_table
    ),
    (
      SELECT
        repeatedfield(root_node_id)
      FROM __temp_root_table
    ),
    (
      SELECT
        repeatedfield(root_target_weight)
      FROM __temp_root_table
    ),
    $is_target_weight_floor
  ) AS dt
);
