tp: pre-aggregate by path_hash in _heap_graph_path_hash_aggregate (#8071)

In `_heap_graph_path_hash_aggregate`
(`android.memory.heap_graph.helpers`), joining `$tab` to
`heap_graph_object` and `heap_graph_class` and projecting string columns
before `GROUP BY path_hash` forced SQLite to decode string and class
columns for every reachable object (`1.7M-4.1M` rows) and build a
multi-column temporary B-tree for the `GROUP BY`.

Pre-aggregate the integer size and count columns by `path_hash` first
using `FROM $tab CROSS JOIN heap_graph_object USING (id) GROUP BY
path_hash` so `Dataframe` consumes the `GROUP BY` sort on `path_hash`
directly in C++ and probes `heap_graph_object.id` via `O(1)` `IdSorted`
lookup. Then join `heap_graph_object` and `heap_graph_class` once per
distinct `path_hash` group (`335K-450K` rows, 5x-12x fewer lookups), and
remove the redundant second `GROUP BY path_hash` on the `[native]`
branch. This speeds up `_heap_graph_path_hash_aggregate` by 1.6x-2.4x.

Change-Id: Ia97e29f876d1e10cfd6e370de1d1d88dcf056b85
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/helpers.sql b/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/helpers.sql
index b89cc37..1696d19 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/helpers.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/helpers.sql
@@ -94,15 +94,17 @@
 RETURNS TableOrSubquery
 AS (
   WITH
-    x AS (
+    -- Pre-aggregate integer columns by path_hash first. CROSS JOIN keeps $tab
+    -- as the outer table so Dataframe consumes GROUP BY path_hash in C++ and
+    -- probes heap_graph_object.id via O(1) IdSorted lookup.
+    agg_by_hash AS (
       SELECT
-        o.graph_sample_ts,
-        o.upid,
+        -- All objects sharing a path_hash have the same (upid, graph_sample_ts,
+        -- root_type, heap_type) and class display name, so MIN(id) picks a
+        -- deterministic representative object to resolve those columns below.
+        MIN(id) AS sample_id,
         path_hash,
         parent_path_hash,
-        coalesce(c.deobfuscated_name, c.name, '') AS name,
-        o.root_type,
-        o.heap_type,
         count() AS self_count,
         -- -1 is a placeholder sentinel (see HeapGraphTracker::GetOrInsertObject
         -- in heap_graph_tracker.cc) for forward-referenced objects in
@@ -111,40 +113,44 @@
         sum(o.native_size > 0) AS self_native_count,
         sum(o.native_size) AS self_native_size
       FROM $tab
-      JOIN heap_graph_object AS o
+      CROSS JOIN heap_graph_object AS o
         USING (id)
-      JOIN heap_graph_class AS c
-        ON o.type_id = c.id
       GROUP BY
         path_hash
     )
   SELECT
-    graph_sample_ts,
-    upid,
-    hash(path_hash, 'native', '') AS path_hash,
-    path_hash AS parent_path_hash,
-    '[native] ' || x.name AS name,
-    root_type,
+    o.graph_sample_ts,
+    o.upid,
+    hash(a.path_hash, 'native', '') AS path_hash,
+    a.path_hash AS parent_path_hash,
+    '[native] ' || coalesce(c.deobfuscated_name, c.name, '') AS name,
+    o.root_type,
     'HEAP_TYPE_NATIVE' AS heap_type,
-    sum(x.self_native_count) AS self_count,
-    sum(x.self_native_size) AS self_size
-  FROM x
+    a.self_native_count AS self_count,
+    a.self_native_size AS self_size
+  FROM agg_by_hash AS a
+  JOIN heap_graph_object AS o
+    ON a.sample_id = o.id
+  JOIN heap_graph_class AS c
+    ON o.type_id = c.id
   WHERE
-    x.self_native_size > 0
-  GROUP BY
-    path_hash
+    a.self_native_size > 0
   UNION ALL
   SELECT
-    graph_sample_ts,
-    upid,
-    path_hash,
-    parent_path_hash,
-    name,
-    root_type,
-    heap_type,
-    self_count,
-    self_size
-  FROM x
+    o.graph_sample_ts,
+    o.upid,
+    a.path_hash,
+    a.parent_path_hash,
+    coalesce(c.deobfuscated_name, c.name, '') AS name,
+    o.root_type,
+    o.heap_type,
+    a.self_count,
+    a.self_size
+  FROM agg_by_hash AS a
+  JOIN heap_graph_object AS o
+    ON a.sample_id = o.id
+  JOIN heap_graph_class AS c
+    ON o.type_id = c.id
   ORDER BY
     path_hash
 );
diff --git a/test/trace_processor/diff_tests/stdlib/android/heap_graph_tests.py b/test/trace_processor/diff_tests/stdlib/android/heap_graph_tests.py
index 8265121..3dc49fd 100644
--- a/test/trace_processor/diff_tests/stdlib/android/heap_graph_tests.py
+++ b/test/trace_processor/diff_tests/stdlib/android/heap_graph_tests.py
@@ -103,6 +103,24 @@
           "java.lang.String",1,666,1,666
         """))
 
+  def test_heap_graph_class_tree_native_size(self):
+    return DiffTestBlueprint(
+        trace=Path('../../parser/profiling/heap_graph_native_size.textproto'),
+        query="""
+          INCLUDE PERFETTO MODULE android.memory.heap_graph.class_tree;
+
+          SELECT p.name AS parent_name, c.name, c.heap_type, c.self_count, c.self_size
+          FROM _heap_graph_class_tree c
+          LEFT JOIN _heap_graph_class_tree p ON c.parent_id = p.id
+          WHERE c.name GLOB '*Bitmap*'
+          ORDER BY c.name;
+        """,
+        out=Csv("""
+          "parent_name","name","heap_type","self_count","self_size"
+          "android.graphics.Bitmap","[native] android.graphics.Bitmap","HEAP_TYPE_NATIVE",1,123456
+          "[NULL]","android.graphics.Bitmap","[NULL]",1,46
+        """))
+
   # The same class loaded by two class loaders has the same name but distinct
   # type_ids. The class tree keys on class name, so the two instances must
   # coalesce into a single node (self_count = 2) rather than splitting.