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, + }, + }; + }, + }); +}