--
-- 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
-- starting node 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 LIMIT 1)
-- );
-- ```
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,
  -- The start node to |graph_table| which will be the root of the reachability
  -- tree.
  start_node_id Expr
)
-- The returned table has the schema (node_id UINT32, parent_node_id UINT32).
-- |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((
    -- Aggregate function to perform a DFS on the nodes on the input graph.
    SELECT __intrinsic_dfs(g.source_node_id, g.dest_node_id, $start_node_id)
    FROM $graph_table g
  ))
  -- 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 UINT32, next_node_id UINT32).
-- |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 UINT32, parent_node_id UINT32).
-- |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
  ) dt
);
