ui: Add experimental CPU samples based slice track.

Change-Id: I8fb28042fa489127bdf14dfbd2c2b851fb4ce2c6
diff --git a/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_track.ts b/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_track.ts
index 4b19146..0d64e46 100644
--- a/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_track.ts
+++ b/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_track.ts
@@ -16,7 +16,10 @@
 import type {Trace} from '../../public/trace';
 import {SourceDataset} from '../../trace_processor/dataset';
 import type {FlamegraphState} from '../../widgets/flamegraph';
-import {createProfilingTrack} from './profiling_track';
+import {
+  createProfilingTrack,
+  createCallstackSlicesTrack,
+} from './profiling_track';
 
 export function createCpuProfileTrack(
   trace: Trace,
@@ -62,3 +65,50 @@
     onDetailsPanelStateChange,
   );
 }
+
+export async function createCpuProfileSlicesTrack(
+  trace: Trace,
+  uri: string,
+  tableName: string,
+  utid: number,
+  detailsPanelState: FlamegraphState | undefined,
+  onDetailsPanelStateChange: (state: FlamegraphState) => void,
+) {
+  return await createCallstackSlicesTrack(
+    trace,
+    uri,
+    {
+      dataset: new SourceDataset({
+        schema: {
+          id: NUM,
+          ts: LONG,
+          callsiteId: NUM,
+        },
+        src: `
+          SELECT
+            id,
+            ts,
+            callsite_id AS callsiteId,
+            utid
+          FROM cpu_profile_stack_sample
+        `,
+        filter: {
+          col: 'utid',
+          eq: utid,
+        },
+      }),
+      callsiteQuery: (ts) => `
+        SELECT callsite_id
+        FROM cpu_profile_stack_sample
+        WHERE ts = ${ts} AND utid = ${utid}
+      `,
+      sqlModule: 'callstacks.stack_profile',
+      metricName: 'CPU Profile Samples',
+      panelTitle: 'CPU Profile Samples',
+      sliceName: 'CPU Sample',
+    },
+    tableName,
+    detailsPanelState,
+    onDetailsPanelStateChange,
+  );
+}
diff --git a/ui/src/plugins/dev.perfetto.CpuProfile/index.ts b/ui/src/plugins/dev.perfetto.CpuProfile/index.ts
index 6fea5a2..9101634 100644
--- a/ui/src/plugins/dev.perfetto.CpuProfile/index.ts
+++ b/ui/src/plugins/dev.perfetto.CpuProfile/index.ts
@@ -16,7 +16,10 @@
 import type {Trace} from '../../public/trace';
 import type {PerfettoPlugin} from '../../public/plugin';
 import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
-import {createCpuProfileTrack} from './cpu_profile_track';
+import {
+  createCpuProfileTrack,
+  createCpuProfileSlicesTrack,
+} from './cpu_profile_track';
 import {getThreadUriPrefix} from '../../public/utils';
 import {exists} from '../../base/utils';
 import {TrackNode} from '../../public/workspace';
@@ -58,6 +61,7 @@
     this.store = ctx.mountStore(CpuProfilePlugin.id, (init) =>
       this.migrateCpuProfilePluginState(init),
     );
+    await ctx.engine.query('INCLUDE PERFETTO MODULE callstacks.stack_profile;');
     const result = await ctx.engine.query(`
       with thread_cpu_sample as (
         select distinct utid
@@ -103,6 +107,123 @@
           },
         ),
       });
+      const slicesUri = `${uri}_slices`;
+      const tableName = `slices_${slicesUri.replace(/[^a-zA-Z0-9]/g, '_')}`;
+      await ctx.engine.query(`
+        CREATE PERFETTO TABLE ${tableName} AS
+        WITH samples AS (
+          SELECT
+            id AS sample_id,
+            ts,
+            LEAD(ts, 1, (SELECT end_ts FROM trace_bounds)) OVER (ORDER BY ts) - ts AS dur,
+            callsite_id
+          FROM cpu_profile_stack_sample
+          WHERE utid = ${utid} AND callsite_id IS NOT NULL
+        ),
+        callstack_path AS (
+          SELECT
+            id AS callsite_id,
+            id AS current_callsite_id,
+            parent_id,
+            frame_id,
+            0 AS depth
+          FROM stack_profile_callsite
+          WHERE id IN (SELECT DISTINCT callsite_id FROM samples)
+
+          UNION ALL
+
+          SELECT
+            p.callsite_id,
+            c.id AS current_callsite_id,
+            c.parent_id,
+            c.frame_id,
+            p.depth + 1 AS depth
+          FROM callstack_path p
+          JOIN stack_profile_callsite c ON p.parent_id = c.id
+        ),
+        path_with_max_depth AS (
+          SELECT
+            callsite_id,
+            frame_id,
+            depth,
+            MAX(depth) OVER (PARTITION BY callsite_id) AS max_depth
+          FROM callstack_path
+        ),
+        raw_slices AS (
+          SELECT
+            s.ts,
+            s.dur,
+            f.name,
+            (p.max_depth - p.depth) AS depth,
+            s.callsite_id AS callsiteId
+          FROM samples s
+          JOIN path_with_max_depth p USING (callsite_id)
+          JOIN stack_profile_frame f ON p.frame_id = f.id
+        ),
+        islands AS (
+          SELECT
+            ts,
+            dur,
+            name,
+            depth,
+            callsiteId,
+            CASE
+              WHEN LAG(ts + dur) OVER (PARTITION BY depth, name ORDER BY ts) >= ts THEN 0
+              ELSE 1
+            END AS is_new_island
+          FROM raw_slices
+        ),
+        island_ids AS (
+          SELECT
+            ts,
+            dur,
+            name,
+            depth,
+            callsiteId,
+            SUM(is_new_island) OVER (PARTITION BY depth, name ORDER BY ts) AS island_id
+          FROM islands
+        )
+        SELECT
+          ROW_NUMBER() OVER (ORDER BY ts) AS id,
+          ts,
+          dur,
+          name,
+          depth,
+          callsiteId
+        FROM (
+          SELECT
+            MIN(ts) AS ts,
+            MAX(ts + dur) - MIN(ts) AS dur,
+            name,
+            depth,
+            MIN(callsiteId) AS callsiteId
+          FROM island_ids
+          GROUP BY depth, name, island_id
+        )
+      `);
+      await ctx.engine.query(
+        `CREATE PERFETTO INDEX ${tableName}_id ON ${tableName}(id);`,
+      );
+      ctx.tracks.registerTrack({
+        uri: slicesUri,
+        tags: {
+          kinds: [CPU_PROFILE_TRACK_KIND],
+          utid,
+          ...(exists(upid) && {upid}),
+        },
+        renderer: await createCpuProfileSlicesTrack(
+          ctx,
+          slicesUri,
+          tableName,
+          utid,
+          store.state.detailsPanelFlamegraphState,
+          (state) => {
+            store.edit((draft) => {
+              draft.detailsPanelFlamegraphState = state;
+            });
+          },
+        ),
+      });
       const group = ctx.plugins
         .getPlugin(ProcessThreadGroupsPlugin)
         .getGroupForThread(utid);
@@ -112,6 +233,12 @@
         sortOrder: -40,
       });
       group?.addChildInOrder(track);
+      const slicesTrack = new TrackNode({
+        uri: slicesUri,
+        name: `${threadName} (CPU Callstack Slices)`,
+        sortOrder: -39,
+      });
+      group?.addChildInOrder(slicesTrack);
     }
 
     ctx.selection.registerAreaSelectionTab(this.createAreaSelectionTab(ctx));
diff --git a/ui/src/plugins/dev.perfetto.CpuProfile/profiling_track.ts b/ui/src/plugins/dev.perfetto.CpuProfile/profiling_track.ts
index b3d1632..ecbc893 100644
--- a/ui/src/plugins/dev.perfetto.CpuProfile/profiling_track.ts
+++ b/ui/src/plugins/dev.perfetto.CpuProfile/profiling_track.ts
@@ -30,7 +30,8 @@
 } from '../../widgets/flamegraph';
 import type {Trace} from '../../public/trace';
 import {SliceTrack} from '../../components/tracks/slice_track';
-import type {SourceDataset} from '../../trace_processor/dataset';
+import {SourceDataset} from '../../trace_processor/dataset';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
 
 /**
  * Configuration for creating a profiling track (CPU profile, perf samples, etc)
@@ -206,3 +207,95 @@
     ),
   );
 }
+
+/**
+ * Creates a profiling track displaying callstack samples as actual slices with depth.
+ */
+export function createCallstackSlicesTrack(
+  trace: Trace,
+  uri: string,
+  config: ProfilingTrackConfig,
+  tableName: string,
+  detailsPanelState: FlamegraphState | undefined,
+  onDetailsPanelStateChange: (state: FlamegraphState) => void,
+) {
+  return SliceTrack.createMaterialized({
+    trace,
+    uri,
+    dataset: new SourceDataset({
+      schema: {
+        id: NUM,
+        ts: LONG,
+        dur: LONG,
+        name: STR,
+        depth: NUM,
+        callsiteId: NUM,
+      },
+      src: tableName,
+    }),
+    sliceName: (row) => row.name,
+    // colorizer: (row) => getColorForSample(row.callsiteId),
+    detailsPanel: (row) => {
+      const ts = Time.fromRaw(row.ts);
+      const metrics: ReadonlyArray<QueryFlamegraphMetric> =
+        metricsFromTableOrSubquery({
+          tableOrSubquery: `
+            (
+              select
+                id,
+                parent_id as parentId,
+                name,
+                mapping_name,
+                source_file || ':' || line_number as source_location,
+                self_count
+              from _callstacks_for_callsites!((
+                \n${config.callsiteQuery(ts)}\n
+              ))
+            )
+          `,
+          tableMetrics: [
+            {
+              name: config.metricName,
+              unit: '',
+              columnName: 'self_count',
+            },
+          ],
+          dependencySql: `include perfetto module ${config.sqlModule}`,
+          unaggregatableProperties: [
+            {name: 'mapping_name', displayName: 'Mapping'},
+          ],
+          aggregatableProperties: [
+            {
+              name: 'source_location',
+              displayName: 'Source Location',
+              mergeAggregation: 'ONE_OR_SUMMARY',
+            },
+          ],
+          nameColumnLabel: 'Symbol',
+        });
+      let state = detailsPanelState ?? Flamegraph.createDefaultState(metrics);
+      if (detailsPanelState === undefined) {
+        onDetailsPanelStateChange(state);
+      }
+      return {
+        load: async () => {},
+        render: () =>
+          renderProfilingDetailsPanel(
+            trace,
+            ts,
+            config,
+            state,
+            (newState) => {
+              state = newState;
+              onDetailsPanelStateChange(newState);
+            },
+            metrics,
+          ),
+        serialization: {
+          schema: FLAMEGRAPH_STATE_SCHEMA.optional(),
+          state: undefined as FlamegraphState | undefined,
+        },
+      };
+    },
+  });
+}
diff --git a/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/index.ts b/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/index.ts
index 5bc468d..d85132e 100644
--- a/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/index.ts
+++ b/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/index.ts
@@ -80,6 +80,7 @@
     this.store = ctx.mountStore(InstrumentsSamplesProfilePlugin.id, (init) =>
       this.migrateInstrumentsSamplesProfilePluginState(init),
     );
+    await ctx.engine.query('INCLUDE PERFETTO MODULE callstacks.stack_profile;');
     const pResult = await ctx.engine.query(`
       select distinct upid
       from instruments_sample
@@ -108,6 +109,80 @@
           },
         ),
       });
+      // const slicesUri = `${uri}_slices`;
+      // const tableName = `slices_${slicesUri.replace(/[^a-zA-Z0-9]/g, '_')}`;
+      // await ctx.engine.query(`
+      //   CREATE TABLE ${tableName} AS
+      //   WITH samples AS (
+      //     SELECT
+      //       p.id AS sample_id,
+      //       ts,
+      //       LEAD(ts, 1, (SELECT end_ts FROM trace_bounds)) OVER (ORDER BY ts) - ts AS dur,
+      //       callsite_id
+      //     FROM instruments_sample p
+      //     JOIN thread USING (utid)
+      //     WHERE callsite_id IS NOT NULL
+      //       AND upid = ${upid}
+      //   ),
+      //   callstack_path AS (
+      //     SELECT
+      //       callsite_id,
+      //       id AS forest_id,
+      //       parent_id AS forest_parent_id,
+      //       name,
+      //       0 AS depth
+      //     FROM _callstack_spc_forest
+      //     WHERE callsite_id IN (SELECT DISTINCT callsite_id FROM samples)
+      //       AND is_leaf_function_in_callsite_frame = 1
+      //
+      //     UNION ALL
+      //
+      //     SELECT
+      //       p.callsite_id,
+      //       f.id AS forest_id,
+      //       f.parent_id AS forest_parent_id,
+      //       f.name,
+      //       p.depth + 1 AS depth
+      //     FROM callstack_path p
+      //     JOIN _callstack_spc_forest f ON p.forest_parent_id = f.id
+      //   ),
+      //   path_with_max_depth AS (
+      //     SELECT
+      //       callsite_id,
+      //       name,
+      //       depth,
+      //       MAX(depth) OVER (PARTITION BY callsite_id) AS max_depth
+      //     FROM callstack_path
+      //   )
+      //   SELECT
+      //     s.sample_id AS id,
+      //     s.ts,
+      //     s.dur,
+      //     p.name,
+      //     (p.max_depth - p.depth) AS depth,
+      //     s.callsite_id AS callsiteId
+      //   FROM samples s
+      //   JOIN path_with_max_depth p USING (callsite_id)
+      // `);
+      // ctx.tracks.registerTrack({
+      //   uri: slicesUri,
+      //   tags: {
+      //     kinds: [INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND],
+      //     upid,
+      //   },
+      //   renderer: createProcessInstrumentsSamplesCallstackSlicesTrack(
+      //     ctx,
+      //     slicesUri,
+      //     tableName,
+      //     upid,
+      //     store.state.detailsPanelFlamegraphState,
+      //     (state) => {
+      //       store.edit((draft) => {
+      //         draft.detailsPanelFlamegraphState = state;
+      //       });
+      //     },
+      //   ),
+      // });
       const group = ctx.plugins
         .getPlugin(ProcessThreadGroupsPlugin)
         .getGroupForProcess(upid);
@@ -117,6 +192,12 @@
         sortOrder: -40,
       });
       group?.addChildInOrder(track);
+      // const slicesTrack = new TrackNode({
+      //   uri: slicesUri,
+      //   name: 'Process Callstack Slices',
+      //   sortOrder: -39,
+      // });
+      // group?.addChildInOrder(slicesTrack);
     }
     const tResult = await ctx.engine.query(`
       select distinct
@@ -163,11 +244,93 @@
           },
         ),
       });
+      // const slicesUri = `${uri}_slices`;
+      // const tableName = `slices_${slicesUri.replace(/[^a-zA-Z0-9]/g, '_')}`;
+      // await ctx.engine.query(`
+      //   CREATE TABLE ${tableName} AS
+      //   WITH samples AS (
+      //     SELECT
+      //       p.id AS sample_id,
+      //       ts,
+      //       LEAD(ts, 1, (SELECT end_ts FROM trace_bounds)) OVER (ORDER BY ts) - ts AS dur,
+      //       callsite_id
+      //     FROM instruments_sample p
+      //     WHERE callsite_id IS NOT NULL
+      //       AND utid = ${utid}
+      //   ),
+      //   callstack_path AS (
+      //     SELECT
+      //       callsite_id,
+      //       id AS forest_id,
+      //       parent_id AS forest_parent_id,
+      //       name,
+      //       0 AS depth
+      //     FROM _callstack_spc_forest
+      //     WHERE callsite_id IN (SELECT DISTINCT callsite_id FROM samples)
+      //       AND is_leaf_function_in_callsite_frame = 1
+      //
+      //     UNION ALL
+      //
+      //     SELECT
+      //       p.callsite_id,
+      //       f.id AS forest_id,
+      //       f.parent_id AS forest_parent_id,
+      //       f.name,
+      //       p.depth + 1 AS depth
+      //     FROM callstack_path p
+      //     JOIN _callstack_spc_forest f ON p.forest_parent_id = f.id
+      //   ),
+      //   path_with_max_depth AS (
+      //     SELECT
+      //       callsite_id,
+      //       name,
+      //       depth,
+      //       MAX(depth) OVER (PARTITION BY callsite_id) AS max_depth
+      //     FROM callstack_path
+      //   )
+      //   SELECT
+      //     s.sample_id AS id,
+      //     s.ts,
+      //     s.dur,
+      //     p.name,
+      //     (p.max_depth - p.depth) AS depth,
+      //     s.callsite_id AS callsiteId
+      //   FROM samples s
+      //   JOIN path_with_max_depth p USING (callsite_id)
+      // `);
+      // ctx.tracks.registerTrack({
+      //   uri: slicesUri,
+      //   tags: {
+      //     kinds: [INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND],
+      //     utid,
+      //     upid: upid ?? undefined,
+      //   },
+      //   renderer: createThreadInstrumentsSamplesCallstackSlicesTrack(
+      //     ctx,
+      //     slicesUri,
+      //     tableName,
+      //     utid,
+      //     store.state.detailsPanelFlamegraphState,
+      //     (state) => {
+      //       store.edit((draft) => {
+      //         draft.detailsPanelFlamegraphState = state;
+      //       });
+      //     },
+      //   ),
+      // });
       const group = ctx.plugins
         .getPlugin(ProcessThreadGroupsPlugin)
         .getGroupForThread(utid);
       const track = new TrackNode({uri, name, sortOrder: -50});
       group?.addChildInOrder(track);
+      // const slicesTrack = new TrackNode({
+      //   uri: slicesUri,
+      //   name: threadName === null
+      //     ? `Thread Callstack Slices ${tid}`
+      //     : `${threadName} Callstack Slices ${tid}`,
+      //   sortOrder: -49,
+      // });
+      // group?.addChildInOrder(slicesTrack);
     }
 
     ctx.onTraceReady.addListener(async () => {
@@ -411,3 +574,105 @@
     onDetailsPanelStateChange,
   );
 }
+
+// export function createProcessInstrumentsSamplesCallstackSlicesTrack(
+//   trace: Trace,
+//   uri: string,
+//   tableName: string,
+//   upid: number,
+//   detailsPanelState: FlamegraphState | undefined,
+//   onDetailsPanelStateChange: (state: FlamegraphState) => void,
+// ) {
+//   return createCallstackSlicesTrack(
+//     trace,
+//     uri,
+//     {
+//       dataset: new SourceDataset({
+//         schema: {
+//           id: NUM,
+//           ts: LONG,
+//           callsiteId: NUM,
+//         },
+//         src: `
+//           SELECT
+//             p.id,
+//             ts,
+//             callsite_id AS callsiteId,
+//             upid
+//           FROM instruments_sample p
+//           JOIN thread USING (utid)
+//           WHERE callsite_id IS NOT NULL
+//           ORDER BY ts
+//         `,
+//         filter: {
+//           col: 'upid',
+//           eq: upid,
+//         },
+//       }),
+//       callsiteQuery: (ts) => `
+//         SELECT p.callsite_id
+//         FROM instruments_sample p
+//         JOIN thread t USING (utid)
+//         WHERE p.ts = ${ts}
+//           AND t.upid = ${upid}
+//       `,
+//       sqlModule: 'appleos.instruments.samples',
+//       metricName: 'Instruments Samples',
+//       panelTitle: 'Instruments Samples',
+//       sliceName: 'Instruments Sample',
+//     },
+//     tableName,
+//     detailsPanelState,
+//     onDetailsPanelStateChange,
+//   );
+// }
+//
+// export function createThreadInstrumentsSamplesCallstackSlicesTrack(
+//   trace: Trace,
+//   uri: string,
+//   tableName: string,
+//   utid: number,
+//   detailsPanelState: FlamegraphState | undefined,
+//   onDetailsPanelStateChange: (state: FlamegraphState) => void,
+// ) {
+//   return createCallstackSlicesTrack(
+//     trace,
+//     uri,
+//     {
+//       dataset: new SourceDataset({
+//         schema: {
+//           id: NUM,
+//           ts: LONG,
+//           callsiteId: NUM,
+//         },
+//         src: `
+//           SELECT
+//             p.id,
+//             ts,
+//             callsite_id AS callsiteId,
+//             utid
+//           FROM instruments_sample p
+//           WHERE callsite_id IS NOT NULL
+//           ORDER BY ts
+//         `,
+//         filter: {
+//           col: 'utid',
+//           eq: utid,
+//         },
+//       }),
+//       callsiteQuery: (ts) => `
+//         SELECT p.callsite_id
+//         FROM instruments_sample p
+//         WHERE p.ts = ${ts}
+//           AND p.utid = ${utid}
+//       `,
+//       sqlModule: 'appleos.instruments.samples',
+//       metricName: 'Instruments Samples',
+//       panelTitle: 'Instruments Samples',
+//       sliceName: 'Instruments Sample',
+//     },
+//     tableName,
+//     detailsPanelState,
+//     onDetailsPanelStateChange,
+//   );
+// }
diff --git a/ui/src/plugins/dev.perfetto.LinuxPerf/index.ts b/ui/src/plugins/dev.perfetto.LinuxPerf/index.ts
index 9f39994..6e3d58f 100644
--- a/ui/src/plugins/dev.perfetto.LinuxPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.LinuxPerf/index.ts
@@ -42,6 +42,7 @@
 import CpuProfilePlugin from '../dev.perfetto.CpuProfile';
 import {SourceDataset} from '../../trace_processor/dataset';
 import {createProfilingTrack} from '../dev.perfetto.CpuProfile/profiling_track';
+import {createPerfCallstackSlicesTrack} from './perf_callstack_slices_track';
 
 const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
 
@@ -77,6 +78,9 @@
     this.store = trace.mountStore(LinuxPerfPlugin.id, (init) =>
       this.migrateLinuxPerfPluginState(init),
     );
+    await trace.engine.query(
+      'INCLUDE PERFETTO MODULE callstacks.stack_profile;',
+    );
     const store = assertExists(this.store);
     await this.cacheCounterTypesPerSession(trace);
     await this.addProcessPerfSamplesTracks(trace, store);
@@ -314,6 +318,124 @@
           },
         ),
       });
+      const slicesUri = `${uri}_slices`;
+      const tableName = `slices_${slicesUri.replace(/[^a-zA-Z0-9]/g, '_')}`;
+      await trace.engine.query(`
+        CREATE TABLE ${tableName} AS
+        WITH samples AS (
+          SELECT
+            p.id AS sample_id,
+            ts,
+            LEAD(ts, 1, (SELECT end_ts FROM trace_bounds)) OVER (ORDER BY ts) - ts AS dur,
+            callsite_id
+          FROM perf_sample AS p
+          WHERE callsite_id IS NOT NULL
+            AND utid = ${utid}
+        ),
+        callstack_path AS (
+          SELECT
+            id AS callsite_id,
+            id AS current_callsite_id,
+            parent_id,
+            frame_id,
+            0 AS depth
+          FROM stack_profile_callsite
+          WHERE id IN (SELECT DISTINCT callsite_id FROM samples)
+
+          UNION ALL
+
+          SELECT
+            p.callsite_id,
+            c.id AS current_callsite_id,
+            c.parent_id,
+            c.frame_id,
+            p.depth + 1 AS depth
+          FROM callstack_path p
+          JOIN stack_profile_callsite c ON p.parent_id = c.id
+        ),
+        path_with_max_depth AS (
+          SELECT
+            callsite_id,
+            frame_id,
+            depth,
+            MAX(depth) OVER (PARTITION BY callsite_id) AS max_depth
+          FROM callstack_path
+        ),
+        raw_slices AS (
+          SELECT
+            s.ts,
+            s.dur,
+            f.name,
+            (p.max_depth - p.depth) AS depth,
+            s.callsite_id AS callsiteId
+          FROM samples s
+          JOIN path_with_max_depth p USING (callsite_id)
+          JOIN stack_profile_frame f ON p.frame_id = f.id
+        ),
+        islands AS (
+          SELECT
+            ts,
+            dur,
+            name,
+            depth,
+            callsiteId,
+            CASE
+              WHEN LAG(ts + dur) OVER (PARTITION BY depth, name ORDER BY ts) >= ts THEN 0
+              ELSE 1
+            END AS is_new_island
+          FROM raw_slices
+        ),
+        island_ids AS (
+          SELECT
+            ts,
+            dur,
+            name,
+            depth,
+            callsiteId,
+            SUM(is_new_island) OVER (PARTITION BY depth, name ORDER BY ts) AS island_id
+          FROM islands
+        )
+        SELECT
+          ROW_NUMBER() OVER (ORDER BY ts) AS id,
+          ts,
+          dur,
+          name,
+          depth,
+          callsiteId
+        FROM (
+          SELECT
+            MIN(ts) AS ts,
+            MAX(ts + dur) - MIN(ts) AS dur,
+            name,
+            depth,
+            MIN(callsiteId) AS callsiteId
+          FROM island_ids
+          GROUP BY depth, name, island_id
+        )
+      `);
+      await trace.engine.query(
+        `CREATE INDEX ${tableName}_id ON ${tableName}(id);`,
+      );
+      trace.tracks.registerTrack({
+        uri: slicesUri,
+        tags: {
+          kinds: [PERF_SAMPLES_PROFILE_TRACK_KIND],
+          utid,
+          upid: upid ?? undefined,
+        },
+        renderer: await createPerfCallstackSlicesTrack(
+          trace,
+          slicesUri,
+          tableName,
+          `utid = ${utid}`,
+          store.state.detailsPanelFlamegraphState,
+          (state) => {
+            store.edit((draft) => {
+              draft.detailsPanelFlamegraphState = state;
+            });
+          },
+        ),
+      });
       const group = trace.plugins
         .getPlugin(ProcessThreadGroupsPlugin)
         .getGroupForThread(utid);
@@ -325,6 +447,13 @@
         sortOrder: -50,
       });
       group?.addChildInOrder(summaryTrack);
+      const summarySlicesTrack = new TrackNode({
+        uri: slicesUri,
+        name: `${threadName ?? 'Thread'} ${tid} callstack slices`,
+        headless: headless,
+        sortOrder: -49,
+      });
+      group?.addChildInOrder(summarySlicesTrack);
 
       // Nested tracks: one per counter being sampled on.
       for (const {cntrName, sessionId} of counters) {
@@ -357,6 +486,133 @@
           sortOrder: -50,
         });
         summaryTrack.addChildInOrder(track);
+
+        const nestedSlicesUri = `${uri}_slices`;
+        const nestedTableName = `slices_${nestedSlicesUri.replace(/[^a-zA-Z0-9]/g, '_')}`;
+        await trace.engine.query(`
+          CREATE TABLE ${nestedTableName} AS
+          WITH samples AS (
+            SELECT
+              p.id AS sample_id,
+              ts,
+              LEAD(ts, 1, (SELECT end_ts FROM trace_bounds)) OVER (ORDER BY ts) - ts AS dur,
+              callsite_id
+            FROM perf_sample AS p
+            WHERE callsite_id IS NOT NULL
+              AND utid = ${utid}
+              AND perf_session_id = ${sessionId}
+          ),
+          callstack_path AS (
+            SELECT
+              id AS callsite_id,
+              id AS current_callsite_id,
+              parent_id,
+              frame_id,
+              0 AS depth
+            FROM stack_profile_callsite
+            WHERE id IN (SELECT DISTINCT callsite_id FROM samples)
+
+            UNION ALL
+
+            SELECT
+              p.callsite_id,
+              c.id AS current_callsite_id,
+              c.parent_id,
+              c.frame_id,
+              p.depth + 1 AS depth
+            FROM callstack_path p
+            JOIN stack_profile_callsite c ON p.parent_id = c.id
+          ),
+          path_with_max_depth AS (
+            SELECT
+              callsite_id,
+              frame_id,
+              depth,
+              MAX(depth) OVER (PARTITION BY callsite_id) AS max_depth
+            FROM callstack_path
+          ),
+          raw_slices AS (
+            SELECT
+              s.ts,
+              s.dur,
+              f.name,
+              (p.max_depth - p.depth) AS depth,
+              s.callsite_id AS callsiteId
+            FROM samples s
+            JOIN path_with_max_depth p USING (callsite_id)
+            JOIN stack_profile_frame f ON p.frame_id = f.id
+          ),
+          islands AS (
+            SELECT
+              ts,
+              dur,
+              name,
+              depth,
+              callsiteId,
+              CASE
+                WHEN LAG(ts + dur) OVER (PARTITION BY depth, name ORDER BY ts) >= ts THEN 0
+                ELSE 1
+              END AS is_new_island
+            FROM raw_slices
+          ),
+          island_ids AS (
+            SELECT
+              ts,
+              dur,
+              name,
+              depth,
+              callsiteId,
+              SUM(is_new_island) OVER (PARTITION BY depth, name ORDER BY ts) AS island_id
+            FROM islands
+          )
+          SELECT
+            ROW_NUMBER() OVER (ORDER BY ts) AS id,
+            ts,
+            dur,
+            name,
+            depth,
+            callsiteId
+          FROM (
+            SELECT
+              MIN(ts) AS ts,
+              MAX(ts + dur) - MIN(ts) AS dur,
+              name,
+              depth,
+              MIN(callsiteId) AS callsiteId
+            FROM island_ids
+            GROUP BY depth, name, island_id
+          )
+        `);
+        await trace.engine.query(
+          `CREATE INDEX ${nestedTableName}_id ON ${nestedTableName}(id);`,
+        );
+        trace.tracks.registerTrack({
+          uri: nestedSlicesUri,
+          tags: {
+            kinds: [PERF_SAMPLES_PROFILE_TRACK_KIND],
+            utid,
+            upid: upid ?? undefined,
+            perfSessionId: sessionId,
+          },
+          renderer: await createPerfCallstackSlicesTrack(
+            trace,
+            nestedSlicesUri,
+            nestedTableName,
+            `utid = ${utid} AND perf_session_id = ${sessionId}`,
+            store.state.detailsPanelFlamegraphState,
+            (state) => {
+              store.edit((draft) => {
+                draft.detailsPanelFlamegraphState = state;
+              });
+            },
+          ),
+        });
+        const nestedSlicesTrack = new TrackNode({
+          uri: nestedSlicesUri,
+          name: `${threadName ?? 'Thread'} ${tid} callstack slices ${cntrName}`,
+          sortOrder: -49,
+        });
+        summaryTrack.addChildInOrder(nestedSlicesTrack);
       }
     }
   }
diff --git a/ui/src/plugins/dev.perfetto.LinuxPerf/perf_callstack_slices_track.ts b/ui/src/plugins/dev.perfetto.LinuxPerf/perf_callstack_slices_track.ts
new file mode 100644
index 0000000..0ff2f5e
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.LinuxPerf/perf_callstack_slices_track.ts
@@ -0,0 +1,136 @@
+// Copyright (C) 2026 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 {LONG, NUM, STR} from '../../trace_processor/query_result';
+import type {Trace} from '../../public/trace';
+import {SourceDataset} from '../../trace_processor/dataset';
+import {SliceTrack} from '../../components/tracks/slice_track';
+import {getColorForSample} from '../../components/colorizer';
+import {
+  metricsFromTableOrSubquery,
+  type QueryFlamegraphMetric,
+} from '../../components/query_flamegraph';
+import {FlamegraphPanel} from '../../components/flamegraph_panel';
+import {FlamegraphProfile} from '../../components/flamegraph_profile';
+import {DetailsShell} from '../../widgets/details_shell';
+import {Timestamp} from '../../components/widgets/timestamp';
+import {Time} from '../../base/time';
+import {
+  Flamegraph,
+  type FlamegraphState,
+  FLAMEGRAPH_STATE_SCHEMA,
+} from '../../widgets/flamegraph';
+import m from 'mithril';
+
+export function createPerfCallstackSlicesTrack(
+  trace: Trace,
+  uri: string,
+  tableName: string,
+  trackConstraints: string,
+  detailsPanelState: FlamegraphState | undefined,
+  onDetailsPanelStateChange: (state: FlamegraphState) => void,
+) {
+  return SliceTrack.createMaterialized({
+    trace,
+    uri,
+    dataset: new SourceDataset({
+      schema: {
+        id: NUM,
+        ts: LONG,
+        dur: LONG,
+        name: STR,
+        depth: NUM,
+        callsiteId: NUM,
+      },
+      src: tableName,
+    }),
+    sliceName: (row) => row.name,
+    colorizer: (row) => getColorForSample(row.callsiteId),
+    detailsPanel: (row) => {
+      const ts = Time.fromRaw(row.ts);
+      const metrics: ReadonlyArray<QueryFlamegraphMetric> =
+        metricsFromTableOrSubquery({
+          tableOrSubquery: `
+            (
+              select
+                id,
+                parent_id as parentId,
+                name,
+                mapping_name,
+                source_file || ':' || line_number as source_location,
+                self_count
+              from _callstacks_for_callsites!((
+                SELECT ps.callsite_id
+                FROM perf_sample ps
+                JOIN thread t USING (utid)
+                WHERE ps.ts = ${ts}
+                  AND ${trackConstraints}
+              ))
+            )
+          `,
+          tableMetrics: [
+            {
+              name: 'Perf Samples',
+              unit: '',
+              columnName: 'self_count',
+            },
+          ],
+          dependencySql: `include perfetto module linux.perf.samples`,
+          unaggregatableProperties: [
+            {name: 'mapping_name', displayName: 'Mapping'},
+          ],
+          aggregatableProperties: [
+            {
+              name: 'source_location',
+              displayName: 'Source Location',
+              mergeAggregation: 'ONE_OR_SUMMARY',
+            },
+          ],
+          nameColumnLabel: 'Symbol',
+        });
+      let state = detailsPanelState ?? Flamegraph.createDefaultState(metrics);
+      if (detailsPanelState === undefined) {
+        onDetailsPanelStateChange(state);
+      }
+      return {
+        load: async () => {},
+        render: () =>
+          m(
+            FlamegraphProfile,
+            m(
+              DetailsShell,
+              {
+                fillHeight: true,
+                title: 'Perf sample',
+                buttons: m('span', 'Timestamp: ', m(Timestamp, {trace, ts})),
+              },
+              m(FlamegraphPanel, {
+                trace,
+                metrics,
+                state,
+                onStateChange: (newState) => {
+                  state = newState;
+                  onDetailsPanelStateChange(newState);
+                },
+              }),
+            ),
+          ),
+        serialization: {
+          schema: FLAMEGRAPH_STATE_SCHEMA.optional(),
+          state: undefined as FlamegraphState | undefined,
+        },
+      };
+    },
+  });
+}