ui: add dominated size and objects tabs to Java heap graph view

Makes use of existing flamegraph view to present heap graph dominator tree stats.

Nodes of a heap graph dominator tree (see tp stdlib memory module) are objects, and we transform that object tree into a class tree:

1. start from the GC root level,
2. merge object nodes with the same type_name and same parent node, also merge their subtrees by re-parenting their children, and
3. do it level by level until we reach the max depth level we want to process.

For example, the object tree below, in which letter represents class name, number represents inclusive sub-tree size (i.e. dominated set object count):
       A(9)
    /   |    \
   A(3) B(3)  B(2)
 / |   / \    |
B  C  C   D   C
becomes
       A(9)
     /     \
   A(3)    B(5)
 /   |      |  \
B(1) C(1)  C(2) D(1)

Bug: 330703412
Change-Id: I03288d3eada158582009e06d2df8447a3bb382db
diff --git a/ui/src/controller/flamegraph_controller.ts b/ui/src/controller/flamegraph_controller.ts
index aa123f7..a10252c 100644
--- a/ui/src/controller/flamegraph_controller.ts
+++ b/ui/src/controller/flamegraph_controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Duration, time} from '../base/time';
+import {Duration, Time, time} from '../base/time';
 import {exists} from '../base/utils';
 import {Actions} from '../common/actions';
 import {
@@ -26,6 +26,7 @@
   FlamegraphState,
   FlamegraphStateViewingOption,
   ProfileType,
+  isHeapGraphDominatorTreeViewingOption,
 } from '../common/state';
 import {FlamegraphDetails, globals} from '../frontend/globals';
 import {publishFlamegraphDetails} from '../frontend/publish';
@@ -115,6 +116,10 @@
   private flamegraphDetails: FlamegraphDetails = {};
   private areaSelectionHandler: AreaSelectionHandler;
   private cache: TablesCache;
+  private heapGraphSelected: {upid: number; timestamp: time} = {
+    upid: -1,
+    timestamp: Time.INVALID,
+  };
 
   constructor(private args: FlamegraphControllerArgs) {
     super('main');
@@ -197,13 +202,12 @@
 
     this.lastSelectedFlamegraphState = {...selection};
 
-    const expandedId = selectedFlamegraphState.expandedCallsite
-      ? selectedFlamegraphState.expandedCallsite.id
-      : -1;
-    const rootSize =
-      selectedFlamegraphState.expandedCallsite === undefined
-        ? undefined
-        : selectedFlamegraphState.expandedCallsite.totalSize;
+    const expandedCallsite =
+      selectedFlamegraphState.expandedCallsiteByViewingOption[
+        selectedFlamegraphState.viewingOption
+      ];
+    const expandedId = expandedCallsite ? expandedCallsite.id : -1;
+    const rootSize = expandedCallsite?.totalSize;
 
     const key = `${selectedFlamegraphState.upids};${selectedFlamegraphState.start};${selectedFlamegraphState.end}`;
 
@@ -238,7 +242,7 @@
           this.lastSelectedFlamegraphState.viewingOption,
           isInAreaSelection,
           rootSize,
-          this.lastSelectedFlamegraphState.expandedCallsite,
+          expandedCallsite,
         );
       }
     } finally {
@@ -264,8 +268,10 @@
         this.lastSelectedFlamegraphState.viewingOption !==
           selection.viewingOption ||
         this.lastSelectedFlamegraphState.focusRegex !== selection.focusRegex ||
-        this.lastSelectedFlamegraphState.expandedCallsite !==
-          selection.expandedCallsite)
+        this.lastSelectedFlamegraphState.expandedCallsiteByViewingOption[
+          selection.viewingOption
+        ] !==
+          selection.expandedCallsiteByViewingOption[selection.viewingOption])
     );
   }
 
@@ -311,7 +317,7 @@
     if (this.flamegraphDatasets.has(key)) {
       currentData = this.flamegraphDatasets.get(key)!;
     } else {
-      // TODO(hjd): Show loading state.
+      // TODO(b/330703412): Show loading state.
 
       // Collecting data for drawing flamegraph for selected profile.
       // Data needs to be in following format:
@@ -322,6 +328,7 @@
         upids,
         type,
         focusRegex,
+        viewingOption,
       );
       currentData = await this.getFlamegraphDataFromTables(
         tableName,
@@ -374,6 +381,18 @@
         totalColumnName = 'cumulativeSize';
         selfColumnName = 'size';
         break;
+      case FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY:
+        orderBy = `where depth < ${maxDepth} order by depth,
+          cumulativeCount desc, name`;
+        totalColumnName = 'cumulativeCount';
+        selfColumnName = 'count';
+        break;
+      case FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY:
+        orderBy = `where depth < ${maxDepth} order by depth,
+          cumulativeSize desc, name`;
+        totalColumnName = 'cumulativeSize';
+        selfColumnName = 'size';
+        break;
       default:
         const exhaustiveCheck: never = viewingOption;
         throw new Error(`Unhandled case: ${exhaustiveCheck}`);
@@ -381,21 +400,21 @@
     }
 
     const callsites = await this.args.engine.query(`
-        SELECT
-        id as hash,
-        IFNULL(IFNULL(DEMANGLE(name), name), '[NULL]') as name,
-        IFNULL(parent_id, -1) as parentHash,
-        depth,
-        cumulative_size as cumulativeSize,
-        cumulative_alloc_size as cumulativeAllocSize,
-        cumulative_count as cumulativeCount,
-        cumulative_alloc_count as cumulativeAllocCount,
-        map_name as mapping,
-        size,
-        count,
-        IFNULL(source_file, '') as sourceFile,
-        IFNULL(line_number, -1) as lineNumber
-        from ${tableName} ${orderBy}`);
+      SELECT
+      id as hash,
+      IFNULL(IFNULL(DEMANGLE(name), name), '[NULL]') as name,
+      IFNULL(parent_id, -1) as parentHash,
+      depth,
+      cumulative_size as cumulativeSize,
+      cumulative_alloc_size as cumulativeAllocSize,
+      cumulative_count as cumulativeCount,
+      cumulative_alloc_count as cumulativeAllocCount,
+      map_name as mapping,
+      size,
+      count,
+      IFNULL(source_file, '') as sourceFile,
+      IFNULL(line_number, -1) as lineNumber
+      from ${tableName} ${orderBy}`);
 
     const flamegraphData: CallsiteInfo[] = [];
     const hashToindex: Map<number, number> = new Map();
@@ -467,6 +486,7 @@
     upids: number[],
     type: ProfileType,
     focusRegex: string,
+    viewingOption: FlamegraphStateViewingOption,
   ): Promise<string> {
     const flamegraphType = getFlamegraphType(type);
     if (type === ProfileType.PERF_SAMPLE) {
@@ -493,6 +513,14 @@
           )`,
       );
     }
+    if (
+      type === ProfileType.JAVA_HEAP_GRAPH &&
+      isHeapGraphDominatorTreeViewingOption(viewingOption)
+    ) {
+      return this.cache.getTableName(
+        await this.loadHeapGraphDominatorTreeQuery(upids[0], end),
+      );
+    }
     return this.cache.getTableName(
       `select id, name, map_name, parent_id, depth, cumulative_size,
           cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
@@ -508,6 +536,91 @@
     );
   }
 
+  private async loadHeapGraphDominatorTreeQuery(upid: number, timestamp: time) {
+    const selectTreeQuery = `
+    -- cache invalidate: upid ${upid}, ts ${timestamp}
+    SELECT * FROM heap_graph_type_dominated`;
+    if (
+      this.heapGraphSelected.upid === upid &&
+      this.heapGraphSelected.timestamp === timestamp
+    ) {
+      return selectTreeQuery;
+    }
+    this.heapGraphSelected = {upid, timestamp};
+    this.args.engine.query(`
+    INCLUDE PERFETTO MODULE memory.heap_graph_dominator_tree;
+
+    -- heap graph dominator tree with objects as nodes and all relavant
+    -- object self stats and dominated stats
+    CREATE PERFETTO TABLE heap_graph_object_dominated AS
+    SELECT
+     node.id,
+     node.idom_id,
+     node.dominated_obj_count,
+     node.dominated_size_bytes + node.dominated_native_size_bytes AS dominated_size,
+     node.depth,
+     obj.type_id,
+     obj.root_type,
+     obj.self_size + obj.native_size AS self_size
+    FROM memory_heap_graph_dominator_tree node
+    JOIN heap_graph_object obj USING(id)
+    WHERE obj.upid = ${upid} AND obj.graph_sample_ts = ${timestamp}
+    -- required to accelerate the recursive cte below
+    ORDER BY idom_id;
+
+    -- calculate for each object node in the dominator tree the
+    -- HASH(path of type_id's from the super root to the object)
+    CREATE PERFETTO TABLE _dominator_tree_path_hash AS
+    WITH RECURSIVE _tree_visitor(id, path, path_hash) AS (
+      SELECT
+        id,
+        CAST(type_id AS text) || '-' || IFNULL(root_type, '') AS path,
+        HASH(
+          CAST(type_id AS text) || '-' || IFNULL(root_type, '')
+        ) AS path_hash
+      FROM heap_graph_object_dominated
+      WHERE depth = 1
+      UNION ALL
+      SELECT
+        child.id,
+        parent.path || '/' || CAST(type_id AS text) AS path,
+        HASH(parent.path || '/' || CAST(type_id AS text)) AS path_hash
+      FROM heap_graph_object_dominated child
+      JOIN _tree_visitor parent ON child.idom_id = parent.id
+    )
+    SELECT * from _tree_visitor
+    ORDER BY id;
+
+    -- merge object nodes with the same path into one "class type node", so the
+    -- end result is a tree where nodes are identified by their types and the
+    -- dominator relationships are preserved.
+    CREATE PERFETTO TABLE heap_graph_type_dominated AS
+    SELECT
+      map.path_hash as id,
+      COALESCE(cls.deobfuscated_name, cls.name, '[NULL]') || IIF(
+        node.root_type IS NOT NULL,
+        ' [' || node.root_type || ']', ''
+      ) AS name,
+      IFNULL(parent_map.path_hash, -1) AS parent_id,
+      node.depth - 1 AS depth,
+      sum(dominated_size) AS cumulative_size,
+      -1 AS cumulative_alloc_size,
+      sum(dominated_obj_count) AS cumulative_count,
+      -1 AS cumulative_alloc_count,
+      '' as map_name,
+      '' as source_file,
+      -1 as line_number,
+      sum(self_size) AS size,
+      count(*) AS count
+    FROM heap_graph_object_dominated node
+    JOIN _dominator_tree_path_hash map USING(id)
+    LEFT JOIN _dominator_tree_path_hash parent_map ON node.idom_id = parent_map.id
+    JOIN heap_graph_class cls ON node.type_id = cls.id
+    GROUP BY map.path_hash, name, parent_id, depth, map_name, source_file, line_number;`);
+
+    return selectTreeQuery;
+  }
+
   getMinSizeDisplayed(
     flamegraphData: CallsiteInfo[],
     rootSize?: number,