tp: add support for aggregate selection of perf samples

Change-Id: Ie4e81ea40947a014c4e8803e6685cac216c244e3
diff --git a/Android.bp b/Android.bp
index 11c9b2a..f6fb6ed 100644
--- a/Android.bp
+++ b/Android.bp
@@ -13178,6 +13178,7 @@
         "src/trace_processor/perfetto_sql/stdlib/linux/memory/general.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/memory/high_watermark.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/memory/process.sql",
+        "src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql",
         "src/trace_processor/perfetto_sql/stdlib/pkvm/hypervisor.sql",
         "src/trace_processor/perfetto_sql/stdlib/prelude/casts.sql",
         "src/trace_processor/perfetto_sql/stdlib/prelude/slices.sql",
diff --git a/BUILD b/BUILD
index 24543ae..210c6c0 100644
--- a/BUILD
+++ b/BUILD
@@ -2708,6 +2708,14 @@
     ],
 )
 
+# GN target: //src/trace_processor/perfetto_sql/stdlib/linux/perf:perf
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_stdlib_linux_perf_perf",
+    srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql",
+    ],
+)
+
 # GN target: //src/trace_processor/perfetto_sql/stdlib/linux:linux
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_linux_linux",
@@ -2843,6 +2851,7 @@
         ":src_trace_processor_perfetto_sql_stdlib_linux_cpu_utilization_utilization",
         ":src_trace_processor_perfetto_sql_stdlib_linux_linux",
         ":src_trace_processor_perfetto_sql_stdlib_linux_memory_memory",
+        ":src_trace_processor_perfetto_sql_stdlib_linux_perf_perf",
         ":src_trace_processor_perfetto_sql_stdlib_pkvm_pkvm",
         ":src_trace_processor_perfetto_sql_stdlib_prelude_prelude",
         ":src_trace_processor_perfetto_sql_stdlib_sched_sched",
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/linux/BUILD.gn
index 7c4dd47..e0fd42f 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/BUILD.gn
@@ -19,5 +19,6 @@
   deps = [
     "cpu",
     "memory",
+    "perf",
   ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/perf/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/linux/perf/BUILD.gn
new file mode 100644
index 0000000..ca58438
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/perf/BUILD.gn
@@ -0,0 +1,19 @@
+# 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 at
+#
+#      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.
+
+import("../../../../../../gn/perfetto_sql.gni")
+
+perfetto_sql_source_set("perf") {
+  sources = [ "samples.sql" ]
+}
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql b/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql
new file mode 100644
index 0000000..593542e
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql
@@ -0,0 +1,39 @@
+--
+-- 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.
+
+CREATE PERFETTO MACRO _perf_callsites_for_samples(samples TableOrSubquery)
+RETURNS TableOrSubquery
+AS
+(
+  WITH cs AS MATERIALIZED (
+    SELECT callsite_id, COUNT() cnt FROM $samples GROUP BY 1
+  )
+  SELECT
+    c.id,
+    c.parent_id AS parentId,
+    COALESCE(f.deobfuscated_name, f.name, '[unknown]') AS name,
+    IFNULL((SELECT cnt FROM cs WHERE cs.callsite_id = c.id), 0) AS self_count
+  FROM graph_reachable_dfs!(
+    (
+        SELECT
+          c.id AS source_node_id,
+          c.parent_id AS dest_node_id
+        FROM stack_profile_callsite c
+    ),
+    (SELECT callsite_id AS node_id FROM cs)
+  ) g
+  JOIN stack_profile_callsite c ON c.id = g.node_id
+  JOIN stack_profile_frame f ON c.frame_id = f.id
+);
diff --git a/ui/src/core_plugins/perf_samples_profile/index.ts b/ui/src/core_plugins/perf_samples_profile/index.ts
index 8124c1b..cf1703f 100644
--- a/ui/src/core_plugins/perf_samples_profile/index.ts
+++ b/ui/src/core_plugins/perf_samples_profile/index.ts
@@ -109,23 +109,15 @@
           ...metricsFromTableOrSubquery(
             `
               (
-                with agg_callsites as (
-                  select p.callsite_id, count() as cnt
+                select *
+                from _perf_callsites_for_samples!((
+                  select p.callsite_id
                   from perf_sample p
                   join thread t using (utid)
                   where p.ts >= ${leftTs}
                     and p.ts <= ${rightTs}
                     and t.upid = ${upid}
-                  group by p.callsite_id
-                )
-                select
-                  c.id,
-                  c.parent_id as parentId,
-                  ifnull(f.deobfuscated_name, f.name) as name,
-                  ifnull(cnt, 0) as self_count
-                from stack_profile_callsite c
-                join stack_profile_frame f on c.frame_id = f.id
-                left join agg_callsites a on c.id = a.callsite_id
+                ))
               )
             `,
             [
@@ -135,6 +127,7 @@
                 columnName: 'self_count',
               },
             ],
+            'INCLUDE PERFETTO MODULE linux.perf.samples',
           ),
         ],
       };
diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts
index f8889bb..4587aa6 100644
--- a/ui/src/frontend/aggregation_tab.ts
+++ b/ui/src/frontend/aggregation_tab.ts
@@ -27,10 +27,16 @@
   LegacyFlamegraphDetailsPanel,
   FlamegraphSelectionParams,
 } from './legacy_flamegraph_panel';
-import {ProfileType, TrackState} from '../common/state';
+import {AreaSelection, ProfileType, TrackState} from '../common/state';
 import {assertExists} from '../base/logging';
 import {Monitor} from '../base/monitor';
 import {PERF_SAMPLES_PROFILE_TRACK_KIND} from '../core/track_kinds';
+import {
+  QueryFlamegraph,
+  QueryFlamegraphAttrs,
+  USE_NEW_FLAMEGRAPH_IMPL,
+  metricsFromTableOrSubquery,
+} from '../core/query_flamegraph';
 
 interface View {
   key: string;
@@ -41,7 +47,8 @@
 class AreaDetailsPanel implements m.ClassComponent {
   private readonly monitor = new Monitor([() => globals.state.selection]);
   private currentTab: string | undefined = undefined;
-  private flamegraphSelection?: FlamegraphSelectionParams;
+  private flamegraphAttrs?: QueryFlamegraphAttrs;
+  private legacyFlamegraphSelection?: FlamegraphSelectionParams;
 
   private getCurrentView(): string | undefined {
     const types = this.getViews().map(({key}) => key);
@@ -62,18 +69,12 @@
   }
 
   private getViews(): View[] {
-    const views = [];
+    const views: View[] = [];
 
-    this.flamegraphSelection = this.computeFlamegraphSelection();
-    if (this.flamegraphSelection !== undefined) {
-      views.push({
-        key: 'flamegraph_selection',
-        name: 'Flamegraph Selection',
-        content: m(LegacyFlamegraphDetailsPanel, {
-          cache: globals.areaFlamegraphCache,
-          selection: this.flamegraphSelection,
-        }),
-      });
+    if (USE_NEW_FLAMEGRAPH_IMPL.get()) {
+      this.addFlamegraphView(views);
+    } else {
+      this.addLegacyFlamegraphView(views);
     }
 
     for (const [key, value] of globals.aggregateDataStore.entries()) {
@@ -155,7 +156,78 @@
     );
   }
 
-  private computeFlamegraphSelection() {
+  private addFlamegraphView(views: View[]) {
+    this.flamegraphAttrs = this.computeFlamegraphAttrs();
+    if (this.flamegraphAttrs === undefined) {
+      return;
+    }
+    views.push({
+      key: 'flamegraph_selection',
+      name: 'Flamegraph Selection',
+      content: m(QueryFlamegraph, this.flamegraphAttrs),
+    });
+  }
+
+  private computeFlamegraphAttrs() {
+    const currentSelection = globals.state.selection;
+    if (currentSelection.kind !== 'area') {
+      return undefined;
+    }
+    if (!this.monitor.ifStateChanged()) {
+      // If the selection has not changed, just return a copy of the last seen
+      // attrs.
+      return this.flamegraphAttrs;
+    }
+    const upids = getUpidsFromAreaSelection(currentSelection);
+    if (upids.length === 0) {
+      return undefined;
+    }
+    return {
+      engine: assertExists(this.getCurrentEngine()),
+      metrics: [
+        ...metricsFromTableOrSubquery(
+          `
+            (
+              select *
+              from _perf_callsites_for_samples!((
+                select p.callsite_id
+                from perf_sample p
+                join thread t using (utid)
+                where p.ts >= ${currentSelection.start}
+                  and p.ts <= ${currentSelection.end}
+                  and t.upid in (${upids.join(',')})
+              ))
+            )
+          `,
+          [
+            {
+              name: 'Perf Samples',
+              unit: '',
+              columnName: 'self_count',
+            },
+          ],
+          'INCLUDE PERFETTO MODULE linux.perf.samples',
+        ),
+      ],
+    };
+  }
+
+  private addLegacyFlamegraphView(views: View[]) {
+    this.legacyFlamegraphSelection = this.computeLegacyFlamegraphSelection();
+    if (this.legacyFlamegraphSelection === undefined) {
+      return;
+    }
+    views.push({
+      key: 'flamegraph_selection',
+      name: 'Flamegraph Selection',
+      content: m(LegacyFlamegraphDetailsPanel, {
+        cache: globals.areaFlamegraphCache,
+        selection: this.legacyFlamegraphSelection,
+      }),
+    });
+  }
+
+  private computeLegacyFlamegraphSelection() {
     const currentSelection = globals.state.selection;
     if (currentSelection.kind !== 'area') {
       return undefined;
@@ -163,17 +235,9 @@
     if (!this.monitor.ifStateChanged()) {
       // If the selection has not changed, just return a copy of the last seen
       // selection.
-      return this.flamegraphSelection;
+      return this.legacyFlamegraphSelection;
     }
-    const upids = [];
-    for (const trackId of currentSelection.tracks) {
-      const track: TrackState | undefined = globals.state.tracks[trackId];
-      const trackInfo = globals.trackManager.resolveTrackInfo(track?.uri);
-      if (trackInfo?.kind !== PERF_SAMPLES_PROFILE_TRACK_KIND) {
-        continue;
-      }
-      upids.push(assertExists(trackInfo.upid));
-    }
+    const upids = getUpidsFromAreaSelection(currentSelection);
     if (upids.length === 0) {
       return undefined;
     }
@@ -184,6 +248,12 @@
       upids,
     };
   }
+
+  private getCurrentEngine() {
+    const engineId = globals.getCurrentEngine()?.id;
+    if (engineId === undefined) return undefined;
+    return globals.engines.get(engineId);
+  }
 }
 
 export class AggregationsTabs implements Disposable {
@@ -207,3 +277,16 @@
     this.trash.dispose();
   }
 }
+
+function getUpidsFromAreaSelection(currentSelection: AreaSelection) {
+  const upids = [];
+  for (const trackId of currentSelection.tracks) {
+    const track: TrackState | undefined = globals.state.tracks[trackId];
+    const trackInfo = globals.trackManager.resolveTrackInfo(track?.uri);
+    if (trackInfo?.kind !== PERF_SAMPLES_PROFILE_TRACK_KIND) {
+      continue;
+    }
+    upids.push(assertExists(trackInfo.upid));
+  }
+  return upids;
+}