ui: Add callstack sampling flamegraph behind flag

Bug: 195934783
Change-Id: I7b1a38ea7fba379e189f1464f2ed16804033a354
diff --git a/src/trace_processor/dynamic/experimental_flamegraph_generator.cc b/src/trace_processor/dynamic/experimental_flamegraph_generator.cc
index 391888e..9af3e33 100644
--- a/src/trace_processor/dynamic/experimental_flamegraph_generator.cc
+++ b/src/trace_processor/dynamic/experimental_flamegraph_generator.cc
@@ -262,7 +262,7 @@
   } else if (values.profile_type == "native") {
     table = BuildNativeHeapProfileFlamegraph(context_->storage.get(),
                                              values.upid, values.ts);
-  } else if (values.profile_type == "callstack") {
+  } else if (values.profile_type == "perf") {
     table = BuildNativeCallStackSamplingFlamegraph(context_->storage.get(),
                                                    values.upid, values.ts);
   }
diff --git a/src/trace_processor/importers/proto/flamegraph_construction_algorithms.cc b/src/trace_processor/importers/proto/flamegraph_construction_algorithms.cc
index 2197bc9..8343a8e 100644
--- a/src/trace_processor/importers/proto/flamegraph_construction_algorithms.cc
+++ b/src/trace_processor/importers/proto/flamegraph_construction_algorithms.cc
@@ -178,11 +178,7 @@
 BuildFlamegraphTableHeapSizeAndCount(
     std::unique_ptr<tables::ExperimentalFlamegraphNodesTable> tbl,
     const std::vector<uint32_t>& callsite_to_merged_callsite,
-    TraceStorage* storage,
     const Table& filtered) {
-  const tables::StackProfileCallsiteTable& callsites_tbl =
-      storage->stack_profile_callsite_table();
-
   for (auto it = filtered.IterateRows(); it; it.Next()) {
     int64_t size =
         it.Get(static_cast<uint32_t>(
@@ -200,8 +196,7 @@
 
     PERFETTO_CHECK((size <= 0 && count <= 0) || (size >= 0 && count >= 0));
     uint32_t merged_idx =
-        callsite_to_merged_callsite[*callsites_tbl.id().IndexOf(
-            CallsiteId(static_cast<uint32_t>(callsite_id)))];
+        callsite_to_merged_callsite[static_cast<unsigned long>(callsite_id)];
     // On old heapprofd producers, the count field is incorrectly set and we
     // zero it in proto_trace_parser.cc.
     // As such, we cannot depend on count == 0 to imply size == 0, so we check
@@ -261,11 +256,7 @@
 BuildFlamegraphTableCallstackSizeAndCount(
     std::unique_ptr<tables::ExperimentalFlamegraphNodesTable> tbl,
     const std::vector<uint32_t>& callsite_to_merged_callsite,
-    TraceStorage* storage,
     const Table& filtered) {
-  const tables::StackProfileCallsiteTable& callsites_tbl =
-      storage->stack_profile_callsite_table();
-
   for (auto it = filtered.IterateRows(); it; it.Next()) {
     int64_t callsite_id =
         it.Get(static_cast<uint32_t>(
@@ -273,8 +264,7 @@
             .long_value;
 
     uint32_t merged_idx =
-        callsite_to_merged_callsite[*callsites_tbl.id().IndexOf(
-            CallsiteId(static_cast<uint32_t>(callsite_id)))];
+        callsite_to_merged_callsite[static_cast<unsigned long>(callsite_id)];
     tbl->mutable_size()->Set(merged_idx, tbl->size()[merged_idx] + 1);
     tbl->mutable_count()->Set(merged_idx, tbl->count()[merged_idx] + 1);
   }
@@ -320,7 +310,7 @@
                                         filtered);
   return BuildFlamegraphTableHeapSizeAndCount(
       std::move(table_and_callsites.tbl),
-      table_and_callsites.callsite_to_merged_callsite, storage, filtered);
+      table_and_callsites.callsite_to_merged_callsite, filtered);
 }
 
 std::unique_ptr<tables::ExperimentalFlamegraphNodesTable>
@@ -353,13 +343,13 @@
   Table filtered_fully =
       filtered_by_pid.Filter({storage->perf_sample_table().ts().le(timestamp)});
 
-  StringId profile_type = storage->InternString("callstack");
+  StringId profile_type = storage->InternString("perf");
   FlamegraphTableAndMergedCallsites table_and_callsites =
       BuildFlamegraphTableTreeStructure(storage, upid, timestamp, profile_type,
                                         filtered_fully);
   return BuildFlamegraphTableCallstackSizeAndCount(
       std::move(table_and_callsites.tbl),
-      table_and_callsites.callsite_to_merged_callsite, storage, filtered_fully);
+      table_and_callsites.callsite_to_merged_callsite, filtered_fully);
 }
 
 }  // namespace trace_processor
diff --git a/test/trace_processor/profiling/callstack_sampling_flamegraph.sql b/test/trace_processor/profiling/callstack_sampling_flamegraph.sql
index 44765c0..f4164b4 100644
--- a/test/trace_processor/profiling/callstack_sampling_flamegraph.sql
+++ b/test/trace_processor/profiling/callstack_sampling_flamegraph.sql
@@ -1 +1 @@
-select * from experimental_flamegraph(7689491063351, 30, 'callstack') limit 10;
+select * from experimental_flamegraph(7689491063351, 30, 'perf') limit 10;
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 37be6dc..a12390a 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -576,9 +576,13 @@
         };
       },
 
-  selectHeapProfile(
-      state: StateDraft,
-      args: {id: number, upid: number, ts: number, type: string}): void {
+  selectHeapProfile(state: StateDraft, args: {
+    id: number,
+    upid: number,
+    ts: number,
+    type: string,
+    viewingOption?: HeapProfileFlamegraphViewingOption
+  }): void {
     state.currentSelection = {
       kind: 'HEAP_PROFILE',
       id: args.id,
@@ -592,7 +596,7 @@
       upid: args.upid,
       ts: args.ts,
       type: args.type,
-      viewingOption: DEFAULT_VIEWING_OPTION,
+      viewingOption: args.viewingOption || DEFAULT_VIEWING_OPTION,
       focusRegex: '',
     };
   },
diff --git a/ui/src/common/feature_flags.ts b/ui/src/common/feature_flags.ts
index 6106344..efd1ccf 100644
--- a/ui/src/common/feature_flags.ts
+++ b/ui/src/common/feature_flags.ts
@@ -46,8 +46,8 @@
 function isFlagOverrides(o: object): o is FlagOverrides {
   const states =
       [OverrideState.TRUE.toString(), OverrideState.FALSE.toString()];
-  for (const [k, v] of Object.entries(o)) {
-    if (typeof k !== 'string' || typeof v !== 'string' || !states.includes(v)) {
+  for (const v of Object.values(o)) {
+    if (typeof v !== 'string' || !states.includes(v)) {
       return false;
     }
   }
diff --git a/ui/src/common/flamegraph_util.ts b/ui/src/common/flamegraph_util.ts
index 2910952..c8070ff 100644
--- a/ui/src/common/flamegraph_util.ts
+++ b/ui/src/common/flamegraph_util.ts
@@ -12,12 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {CallsiteInfo} from '../common/state';
+import {CallsiteInfo} from './state';
 
 export const SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY = 'SPACE';
 export const ALLOC_SPACE_MEMORY_ALLOCATED_KEY = 'ALLOC_SPACE';
 export const OBJECTS_ALLOCATED_NOT_FREED_KEY = 'OBJECTS';
 export const OBJECTS_ALLOCATED_KEY = 'ALLOC_OBJECTS';
+export const PERF_SAMPLES_KEY = 'PERF_SAMPLES';
 
 export const DEFAULT_VIEWING_OPTION = SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY;
 
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 7138d06..c73dd5c 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -77,7 +77,7 @@
 }
 
 export type HeapProfileFlamegraphViewingOption =
-    'SPACE'|'ALLOC_SPACE'|'OBJECTS'|'ALLOC_OBJECTS';
+    'SPACE'|'ALLOC_SPACE'|'OBJECTS'|'ALLOC_OBJECTS'|'PERF_SAMPLES';
 
 export interface CallsiteInfo {
   id: number;
diff --git a/ui/src/controller/heap_profile_controller.ts b/ui/src/controller/heap_profile_controller.ts
index 8ec8e34..ef9df65 100644
--- a/ui/src/controller/heap_profile_controller.ts
+++ b/ui/src/controller/heap_profile_controller.ts
@@ -21,6 +21,7 @@
   mergeCallsites,
   OBJECTS_ALLOCATED_KEY,
   OBJECTS_ALLOCATED_NOT_FREED_KEY,
+  PERF_SAMPLES_KEY,
   SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY
 } from '../common/flamegraph_util';
 import {NUM, STR} from '../common/query_result';
@@ -236,13 +237,6 @@
     // Alternatively consider collapsing frames of the same label.
     const maxDepth = 100;
     switch (viewingOption) {
-      case SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY:
-        orderBy = `where cumulative_size > 0 and depth < ${
-            maxDepth} order by depth, parent_id,
-            cumulative_size desc, name`;
-        totalColumnName = 'cumulativeSize';
-        selfColumnName = 'size';
-        break;
       case ALLOC_SPACE_MEMORY_ALLOCATED_KEY:
         orderBy = `where cumulative_alloc_size > 0 and depth < ${
             maxDepth} order by depth, parent_id,
@@ -264,6 +258,14 @@
         totalColumnName = 'cumulativeAllocCount';
         selfColumnName = 'count';
         break;
+      case PERF_SAMPLES_KEY:
+      case SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY:
+        orderBy = `where cumulative_size > 0 and depth < ${
+            maxDepth} order by depth, parent_id,
+            cumulative_size desc, name`;
+        totalColumnName = 'cumulativeSize';
+        selfColumnName = 'size';
+        break;
       default:
         break;
     }
@@ -285,7 +287,7 @@
         IFNULL(line_number, -1) as lineNumber
         from ${tableName} ${orderBy}`);
 
-    const flamegraphData: CallsiteInfo[] = new Array();
+    const flamegraphData: CallsiteInfo[] = [];
     const hashToindex: Map<number, number> = new Map();
     const it = callsites.iter({
       hash: NUM,
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index fefb2ae..bcb2b4c 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -21,6 +21,7 @@
 import {TRACE_MARGIN_TIME_S} from '../common/constants';
 import {Engine} from '../common/engine';
 import {featureFlags, Flag} from '../common/feature_flags';
+import {PERF_SAMPLES_KEY} from '../common/flamegraph_util';
 import {HttpRpcEngine} from '../common/http_rpc_engine';
 import {NUM, NUM_NULL, QueryError, STR, STR_NULL} from '../common/query_result';
 import {EngineMode} from '../common/state';
@@ -116,6 +117,12 @@
   return [flag, m];
 });
 
+const PERF_SAMPLE_FLAG = featureFlags.register({
+  id: 'perfSampleFlamegraph',
+  name: 'Perf Sample Flamegraph',
+  description: 'Show flamegraph generated by a perf sample.',
+  defaultValue: false
+});
 
 // TraceController handles handshakes with the frontend for everything that
 // concerns a single trace. It owns the WASM trace processor engine, handles
@@ -391,11 +398,29 @@
 
     globals.dispatch(Actions.removeDebugTrack({}));
     globals.dispatch(Actions.sortThreadTracks({}));
+
     await this.selectFirstHeapProfile();
+    if (PERF_SAMPLE_FLAG.get()) {
+      await this.selectPerfSample();
+    }
 
     return engineMode;
   }
 
+  private async selectPerfSample() {
+    const query = `select ts, upid
+        from perf_sample
+        join thread using (utid)
+        order by ts desc limit 1`;
+    const profile = await assertExists(this.engine).query(query);
+    if (profile.numRows() !== 1) return;
+    const row = profile.firstRow({ts: NUM, upid: NUM});
+    const ts = row.ts;
+    const upid = row.upid;
+    globals.dispatch(Actions.selectHeapProfile(
+        {id: 0, upid, ts, type: 'perf', viewingOption: PERF_SAMPLES_KEY}));
+  }
+
   private async selectFirstHeapProfile() {
     const query = `select * from
     (select distinct(ts) as ts, 'native' as type,
diff --git a/ui/src/frontend/heap_profile_panel.ts b/ui/src/frontend/heap_profile_panel.ts
index 7aadd2f..53b873e 100644
--- a/ui/src/frontend/heap_profile_panel.ts
+++ b/ui/src/frontend/heap_profile_panel.ts
@@ -14,11 +14,13 @@
 
 import * as m from 'mithril';
 
+import {assertExists} from '../base/logging';
 import {Actions} from '../common/actions';
 import {
   ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
   OBJECTS_ALLOCATED_KEY,
   OBJECTS_ALLOCATED_NOT_FREED_KEY,
+  PERF_SAMPLES_KEY,
   SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
 } from '../common/flamegraph_util';
 import {
@@ -42,6 +44,7 @@
 enum ProfileType {
   NATIVE_HEAP_PROFILE = 'native',
   JAVA_HEAP_GRAPH = 'graph',
+  PERF_SAMPLE = 'perf'
 }
 
 function isProfileType(s: string): s is ProfileType {
@@ -169,6 +172,8 @@
         return 'Heap Profile:';
       case ProfileType.JAVA_HEAP_GRAPH:
         return 'Java Heap:';
+      case ProfileType.PERF_SAMPLE:
+        return 'Perf sample:';
       default:
         throw new Error('unknown type');
     }
@@ -181,14 +186,15 @@
     const viewingOption =
         globals.state.currentHeapProfileFlamegraph!.viewingOption;
     switch (this.profileType) {
-      case ProfileType.NATIVE_HEAP_PROFILE:
-        return RENDER_SELF_AND_TOTAL;
       case ProfileType.JAVA_HEAP_GRAPH:
         if (viewingOption === OBJECTS_ALLOCATED_NOT_FREED_KEY) {
           return RENDER_OBJ_COUNT;
         } else {
           return RENDER_SELF_AND_TOTAL;
         }
+      case ProfileType.NATIVE_HEAP_PROFILE:
+      case ProfileType.PERF_SAMPLE:
+        return RENDER_SELF_AND_TOTAL;
       default:
         throw new Error('unknown type');
     }
@@ -200,53 +206,11 @@
     }));
   }
 
-  getButtonsClass(button: HeapProfileFlamegraphViewingOption): string {
-    if (globals.state.currentHeapProfileFlamegraph === null) return '';
-    return globals.state.currentHeapProfileFlamegraph.viewingOption === button ?
-        '.chosen' :
-        '';
-  }
-
   getViewingOptionButtons(): m.Children {
-    const viewingOptions = [
-      m(`button${this.getButtonsClass(SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY)}`,
-        {
-          onclick: () => {
-            this.changeViewingOption(SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY);
-          }
-        },
-        'space'),
-      m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_NOT_FREED_KEY)}`,
-        {
-          onclick: () => {
-            this.changeViewingOption(OBJECTS_ALLOCATED_NOT_FREED_KEY);
-          }
-        },
-        'objects'),
-    ];
-
-    if (this.profileType === ProfileType.NATIVE_HEAP_PROFILE) {
-      viewingOptions.push(
-          m(`button${this.getButtonsClass(ALLOC_SPACE_MEMORY_ALLOCATED_KEY)}`,
-            {
-              onclick: () => {
-                this.changeViewingOption(ALLOC_SPACE_MEMORY_ALLOCATED_KEY);
-              }
-            },
-            'alloc space'),
-          m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_KEY)}`,
-            {
-              onclick: () => {
-                this.changeViewingOption(OBJECTS_ALLOCATED_KEY);
-              }
-            },
-            'alloc objects'));
-    }
-    return m('div', ...viewingOptions);
-  }
-
-  changeViewingOption(viewingOption: HeapProfileFlamegraphViewingOption) {
-    globals.dispatch(Actions.changeViewHeapProfileFlamegraph({viewingOption}));
+    return m(
+        'div',
+        ...HeapProfileDetailsPanel.selectViewingOptions(
+            assertExists(this.profileType)));
   }
 
   downloadPprof() {
@@ -294,4 +258,47 @@
   onMouseOut() {
     this.flamegraph.onMouseOut();
   }
+
+  private static selectViewingOptions(profileType: ProfileType) {
+    switch (profileType) {
+      case ProfileType.PERF_SAMPLE:
+        return [this.buildButtonComponent(PERF_SAMPLES_KEY, 'samples')];
+      case ProfileType.NATIVE_HEAP_PROFILE:
+        return [
+          this.buildButtonComponent(
+              SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'space'),
+          this.buildButtonComponent(OBJECTS_ALLOCATED_NOT_FREED_KEY, 'objects')
+        ];
+      case ProfileType.JAVA_HEAP_GRAPH:
+        return [
+          this.buildButtonComponent(
+              SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'space'),
+          this.buildButtonComponent(OBJECTS_ALLOCATED_NOT_FREED_KEY, 'objects'),
+          this.buildButtonComponent(
+              ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 'alloc space'),
+          this.buildButtonComponent(OBJECTS_ALLOCATED_KEY, 'alloc objects')
+        ];
+      default:
+        throw new Error(`Unexpected profile type ${profileType}`);
+    }
+  }
+
+  private static buildButtonComponent(
+      viewingOption: HeapProfileFlamegraphViewingOption, text: string) {
+    const buttonsClass =
+        (globals.state.currentHeapProfileFlamegraph &&
+         globals.state.currentHeapProfileFlamegraph.viewingOption ===
+             viewingOption) ?
+        '.chosen' :
+        '';
+    return m(
+        `button${buttonsClass}`,
+        {
+          onclick: () => {
+            globals.dispatch(
+                Actions.changeViewHeapProfileFlamegraph({viewingOption}));
+          }
+        },
+        text);
+  }
 }