[stdlib]: Add a generic critical path graph module
This brings us closer to productionizing and generalizing
the critical path algorithm. Still in experimental until the API
is fully fledged.
2 functions extracted out:
1. _critical_path(graph_table, root_table)
2. _critical_path_intervals(graph_table, root_table, interval_table)
The first works completely in id space, the second 'flattens' for the UI
Change-Id: I04130e421eb7ccdb43965fccc01e809c6afbae61
diff --git a/Android.bp b/Android.bp
index 11c9b2a..279cfe9 100644
--- a/Android.bp
+++ b/Android.bp
@@ -13163,6 +13163,7 @@
"src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/slices.sql",
"src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/timestamps.sql",
"src/trace_processor/perfetto_sql/stdlib/export/to_firefox_profile.sql",
+ "src/trace_processor/perfetto_sql/stdlib/graphs/critical_path.sql",
"src/trace_processor/perfetto_sql/stdlib/graphs/dominator_tree.sql",
"src/trace_processor/perfetto_sql/stdlib/graphs/partition.sql",
"src/trace_processor/perfetto_sql/stdlib/graphs/scan.sql",
diff --git a/BUILD b/BUILD
index 24543ae..cf94216 100644
--- a/BUILD
+++ b/BUILD
@@ -2662,6 +2662,7 @@
perfetto_filegroup(
name = "src_trace_processor_perfetto_sql_stdlib_graphs_graphs",
srcs = [
+ "src/trace_processor/perfetto_sql/stdlib/graphs/critical_path.sql",
"src/trace_processor/perfetto_sql/stdlib/graphs/dominator_tree.sql",
"src/trace_processor/perfetto_sql/stdlib/graphs/partition.sql",
"src/trace_processor/perfetto_sql/stdlib/graphs/scan.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/graphs/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/graphs/BUILD.gn
index 1710494..5167835 100644
--- a/src/trace_processor/perfetto_sql/stdlib/graphs/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/graphs/BUILD.gn
@@ -16,6 +16,7 @@
perfetto_sql_source_set("graphs") {
sources = [
+ "critical_path.sql",
"dominator_tree.sql",
"partition.sql",
"scan.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/graphs/critical_path.sql b/src/trace_processor/perfetto_sql/stdlib/graphs/critical_path.sql
new file mode 100644
index 0000000..652f8c0
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/graphs/critical_path.sql
@@ -0,0 +1,190 @@
+--
+-- 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.
+--
+
+INCLUDE PERFETTO MODULE graphs.search;
+
+-- Computes critical paths, the dependency graph of a task.
+-- The critical path is a set of edges reachable from a root node with the sum of the edge
+-- weights just exceeding the root node capacity. This ensures that the tasks in the critical path
+-- completely 'covers' the root capacity.
+-- Typically, every node represents a point in time on some task where it transitioned from
+-- idle to active state.
+--
+-- Example usage on traces with Linux sched information:
+-- ```
+-- -- Compute the userspace critical path from every task sleep.
+-- SELECT * FROM
+-- critical_path_intervals!(
+-- _wakeup_userspace_edges,
+-- (SELECT id AS root_node_id, prev_id - id FROM _wakeup_graph WHERE prev_id IS NOT NULL));
+-- ```
+CREATE PERFETTO MACRO _critical_path(
+ -- 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", "dest_node_id" and "edge_weight" corresponding to the two nodes on
+ -- either end of the edges in the graph and the edge weight.
+ --
+ -- 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.
+ -- An edge weight is the absolute difference between the node ids forming the edge.
+ 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 "capacity" corresponding to the starting node id and the capacity
+ -- of the root node to contain edge weights.
+ --
+ -- 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)
+ -- The returned table has the schema (root_id UINT32, id UINT32, parent_id UINT32).
+ -- |root_id| is the id of the root where the critical path computation started.
+ -- |id| is the id of a node in the critical path and |parent_id| is the predecessor of |id|.
+RETURNS TableOrSubQuery
+AS (
+ WITH
+ _edges AS (
+ SELECT source_node_id, dest_node_id, edge_weight FROM $graph_table
+ ),
+ _roots AS (
+ SELECT
+ root_node_id,
+ capacity AS root_target_weight
+ FROM $root_table
+ ),
+ _search_bounds AS (
+ SELECT MIN(root_node_id - root_target_weight) AS min_wakeup,
+ MAX(root_node_id + root_target_weight) AS max_wakeup
+ FROM _roots
+ ),
+ _graph AS (
+ SELECT
+ source_node_id,
+ COALESCE(dest_node_id, source_node_id) AS dest_node_id,
+ edge_weight
+ FROM _edges
+ JOIN _search_bounds
+ WHERE source_node_id BETWEEN min_wakeup AND max_wakeup AND source_node_id IS NOT NULL
+ )
+ SELECT DISTINCT
+ root_node_id AS root_id,
+ parent_node_id AS parent_id,
+ node_id AS id
+ FROM graph_reachable_weight_bounded_dfs !(_graph, _roots, 1) cr
+);
+
+-- Flattens overlapping tasks within a critical path and flattens overlapping critical paths.
+CREATE PERFETTO MACRO _critical_path_to_intervals(critical_path_table TableOrSubquery,
+ node_table TableOrSubquery)
+RETURNS TableOrSubquery
+AS (
+ WITH flat_tasks AS (
+ SELECT
+ node.ts,
+ cr.root_id,
+ cr.id,
+ LEAD(node.ts) OVER (PARTITION BY cr.root_id ORDER BY cr.id) - node.ts AS dur
+ FROM $critical_path_table cr
+ JOIN $node_table node USING(id)
+ ), span_starts AS (
+ SELECT
+ MAX(cr.ts, idle.ts - idle_dur) AS ts,
+ idle.ts AS idle_end_ts,
+ cr.ts + cr.dur AS cr_end_ts,
+ cr.id,
+ cr.root_id
+ FROM flat_tasks cr
+ JOIN $node_table idle ON cr.root_id = idle.id
+ )
+ SELECT
+ ts,
+ MIN(cr_end_ts, idle_end_ts) - ts AS dur,
+ id,
+ root_id
+ FROM span_starts
+ WHERE MIN(idle_end_ts, cr_end_ts) - ts > 0
+);
+
+-- Computes critical paths, the dependency graph of a task and returns a flattened view suitable
+-- for displaying in a UI track without any overlapping intervals.
+-- See the _critical_path MACRO above.
+--
+-- Example usage on traces with Linux sched information:
+-- ```
+-- -- Compute the userspace critical path from every task sleep.
+-- SELECT * FROM
+-- critical_path_intervals!(
+-- _wakeup_userspace_edges,
+-- (SELECT id AS root_node_id, prev_id - id FROM _wakeup_graph WHERE prev_id IS NOT NULL),
+-- _wakeup_intervals);
+-- ```
+CREATE PERFETTO MACRO _critical_path_intervals(
+ -- 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", "dest_node_id" and "edge_weight" corresponding to the two nodes on
+ -- either end of the edges in the graph and the edge weight.
+ --
+ -- 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.
+ -- An edge weight is the absolute difference between the node ids forming the edge.
+ 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 "capacity" corresponding to the starting node id and the capacity
+ -- of the root node to contain edge weights.
+ --
+ -- 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,
+ -- A table/view/subquery corresponding to the idle to active transition points on a task.
+ -- This table must have the columns, "id", "ts", "dur" and "idle_dur". ts and dur is the
+ -- timestamp when the task became active and how long it was active for respectively. idle_dur
+ -- is the duration it was idle for before it became active at "ts".
+ --
+ -- 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.
+ -- There should be one row for every node id encountered in the |graph_table|.
+ interval_table TableOrSubQuery)
+-- The returned table has the schema (id UINT32, ts INT64, dur INT64, idle_dur INT64).
+-- |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 _critical_path_nodes AS (
+ SELECT root_id, id FROM _critical_path!($graph_table, $root_table)
+ ) SELECT root_id, id, ts, dur
+ FROM _critical_path_to_intervals !(_critical_path_nodes, $interval_table)
+ UNION ALL
+ SELECT node.id AS root_id, node.id, node.ts, node.dur
+ FROM $interval_table node
+ JOIN $root_table ON root_node_id = id
+);
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index 534344f..b57a139 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -110,6 +110,7 @@
from diff_tests.stdlib.counters.tests import StdlibCounterIntervals
from diff_tests.stdlib.dynamic_tables.tests import DynamicTables
from diff_tests.stdlib.export.tests import ExportTests
+from diff_tests.stdlib.graphs.critical_path_tests import CriticalPathTests
from diff_tests.stdlib.graphs.dominator_tree_tests import DominatorTree
from diff_tests.stdlib.graphs.partition_tests import GraphPartitionTests
from diff_tests.stdlib.graphs.scan_tests import GraphScanTests
@@ -275,6 +276,7 @@
*AndroidStdlib(index_path, 'stdlib/android', 'AndroidStdlib').fetch(),
*LinuxCpu(index_path, 'stdlib/linux/cpu', 'LinuxCpu').fetch(),
*DominatorTree(index_path, 'stdlib/graphs', 'DominatorTree').fetch(),
+ *CriticalPathTests(index_path, 'stdlib/graphs', 'CriticalPath').fetch(),
*GraphScanTests(index_path, 'stdlib/graphs', 'GraphScan').fetch(),
*ExportTests(index_path, 'stdlib/export', 'ExportTests').fetch(),
*Frames(index_path, 'stdlib/android', 'Frames').fetch(),
diff --git a/test/trace_processor/diff_tests/stdlib/graphs/critical_path_tests.py b/test/trace_processor/diff_tests/stdlib/graphs/critical_path_tests.py
new file mode 100644
index 0000000..7b746aa
--- /dev/null
+++ b/test/trace_processor/diff_tests/stdlib/graphs/critical_path_tests.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+# Copyright (C) 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 a
+#
+# http://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.
+
+from python.generators.diff_tests.testing import DataPath
+from python.generators.diff_tests.testing import Csv
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+class CriticalPathTests(TestSuite):
+
+ def test_critical_path_empty(self):
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE graphs.critical_path;
+
+ WITH edge AS (
+ SELECT 0 as source_node_id, 0 AS dest_node_id
+ WHERE FALSE
+ ), root AS (
+ SELECT 0 as root_node_id, 0 AS capacity
+ WHERE FALSE
+ )
+ SELECT * FROM _critical_path!(
+ (SELECT *, source_node_id - dest_node_id AS edge_weight FROM edge),
+ root
+ );
+ """,
+ out=Csv("""
+ "root_id","parent_id","id"
+ """))
+
+ def test_critical_path(self):
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE graphs.critical_path;
+
+ WITH edge(source_node_id, dest_node_id) AS (
+ values(8, 7), (7, 6), (6, 5), (6, 4), (4, 1), (5, 3), (3, 0)
+ ), root(root_node_id, capacity) AS (
+ values(8, 6)
+ )
+ SELECT * FROM _critical_path!(
+ (SELECT *, source_node_id - dest_node_id AS edge_weight FROM edge),
+ root
+ );
+ """,
+ out=Csv("""
+ "root_id","parent_id","id"
+ 8,"[NULL]",8
+ 8,3,0
+ 8,5,3
+ 8,6,5
+ 8,7,6
+ 8,8,7
+ """))
+
+ def test_critical_path_intervals(self):
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE graphs.critical_path;
+
+ WITH edge(source_node_id, dest_node_id) AS (
+ values(8, 7), (7, 6), (6, 5), (6, 4), (4, 1), (5, 3), (3, 0)
+ ), root(root_node_id, capacity) AS (
+ values(8, 6)
+ ), interval(id, ts, dur, idle_dur) AS (
+ values(8, 8, 1, 6),
+ (7, 7, 1, 1),
+ (6, 6, 1, 1),
+ (5, 5, 1, 1),
+ (4, 4, 1, 1),
+ (3, 3, 1, 1),
+ (2, 2, 1, 1),
+ (1, 1, 1, 1)
+ )
+ SELECT * FROM _critical_path_intervals!(
+ (SELECT *, source_node_id - dest_node_id AS edge_weight FROM edge),
+ root,
+ interval
+ );
+ """,
+ out=Csv("""
+ "root_id","id","ts","dur"
+ 8,3,3,2
+ 8,5,5,1
+ 8,6,6,1
+ 8,7,7,1
+ 8,8,8,1
+ """))