1// Copyright (C) 2025 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {TrackData} from '../../components/tracks/track_data'; 16import {INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND} from '../../public/track_kinds'; 17import {Trace} from '../../public/trace'; 18import {PerfettoPlugin} from '../../public/plugin'; 19import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result'; 20import {assertExists} from '../../base/logging'; 21import { 22 createProcessInstrumentsSamplesProfileTrack, 23 createThreadInstrumentsSamplesProfileTrack, 24} from './instruments_samples_profile_track'; 25import {getThreadUriPrefix} from '../../public/utils'; 26import {TrackNode} from '../../public/workspace'; 27import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups'; 28import {AreaSelection, areaSelectionsEqual} from '../../public/selection'; 29import { 30 metricsFromTableOrSubquery, 31 QueryFlamegraph, 32} from '../../components/query_flamegraph'; 33import {Flamegraph} from '../../widgets/flamegraph'; 34 35export interface Data extends TrackData { 36 tsStarts: BigInt64Array; 37} 38 39function makeUriForProc(upid: number) { 40 return `/process_${upid}/instruments_samples_profile`; 41} 42 43export default class implements PerfettoPlugin { 44 static readonly id = 'dev.perfetto.InstrumentsSamplesProfile'; 45 static readonly dependencies = [ProcessThreadGroupsPlugin]; 46 47 async onTraceLoad(ctx: Trace): Promise<void> { 48 const pResult = await ctx.engine.query(` 49 select distinct upid 50 from instruments_sample 51 join thread using (utid) 52 where callsite_id is not null and upid is not null 53 `); 54 for (const it = pResult.iter({upid: NUM}); it.valid(); it.next()) { 55 const upid = it.upid; 56 const uri = makeUriForProc(upid); 57 const title = `Process Callstacks`; 58 ctx.tracks.registerTrack({ 59 uri, 60 title, 61 tags: { 62 kind: INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND, 63 upid, 64 }, 65 track: createProcessInstrumentsSamplesProfileTrack(ctx, uri, upid), 66 }); 67 const group = ctx.plugins 68 .getPlugin(ProcessThreadGroupsPlugin) 69 .getGroupForProcess(upid); 70 const track = new TrackNode({uri, title, sortOrder: -40}); 71 group?.addChildInOrder(track); 72 } 73 const tResult = await ctx.engine.query(` 74 select distinct 75 utid, 76 tid, 77 thread.name as threadName, 78 upid 79 from instruments_sample 80 join thread using (utid) 81 where callsite_id is not null 82 `); 83 for ( 84 const it = tResult.iter({ 85 utid: NUM, 86 tid: NUM, 87 threadName: STR_NULL, 88 upid: NUM_NULL, 89 }); 90 it.valid(); 91 it.next() 92 ) { 93 const {threadName, utid, tid, upid} = it; 94 const title = 95 threadName === null 96 ? `Thread Callstacks ${tid}` 97 : `${threadName} Callstacks ${tid}`; 98 const uri = `${getThreadUriPrefix(upid, utid)}_instruments_samples_profile`; 99 ctx.tracks.registerTrack({ 100 uri, 101 title, 102 tags: { 103 kind: INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND, 104 utid, 105 upid: upid ?? undefined, 106 }, 107 track: createThreadInstrumentsSamplesProfileTrack(ctx, uri, utid), 108 }); 109 const group = ctx.plugins 110 .getPlugin(ProcessThreadGroupsPlugin) 111 .getGroupForThread(utid); 112 const track = new TrackNode({uri, title, sortOrder: -50}); 113 group?.addChildInOrder(track); 114 } 115 116 ctx.onTraceReady.addListener(async () => { 117 await selectInstrumentsSample(ctx); 118 }); 119 120 ctx.selection.registerAreaSelectionTab(createAreaSelectionTab(ctx)); 121 } 122} 123 124async function selectInstrumentsSample(ctx: Trace) { 125 const profile = await assertExists(ctx.engine).query(` 126 select upid 127 from instruments_sample 128 join thread using (utid) 129 where callsite_id is not null 130 order by ts desc 131 limit 1 132 `); 133 if (profile.numRows() !== 1) return; 134 const row = profile.firstRow({upid: NUM}); 135 const upid = row.upid; 136 137 // Create an area selection over the first process with a instruments samples track 138 ctx.selection.selectArea({ 139 start: ctx.traceInfo.start, 140 end: ctx.traceInfo.end, 141 trackUris: [makeUriForProc(upid)], 142 }); 143} 144 145function createAreaSelectionTab(trace: Trace) { 146 let previousSelection: undefined | AreaSelection; 147 let flamegraph: undefined | QueryFlamegraph; 148 149 return { 150 id: 'instruments_sample_flamegraph', 151 name: 'Instruments Sample Flamegraph', 152 render(selection: AreaSelection) { 153 const changed = 154 previousSelection === undefined || 155 !areaSelectionsEqual(previousSelection, selection); 156 157 if (changed) { 158 flamegraph = computeInstrumentsSampleFlamegraph(trace, selection); 159 previousSelection = selection; 160 } 161 162 if (flamegraph === undefined) { 163 return undefined; 164 } 165 166 return {isLoading: false, content: flamegraph.render()}; 167 }, 168 }; 169} 170 171function computeInstrumentsSampleFlamegraph( 172 trace: Trace, 173 currentSelection: AreaSelection, 174) { 175 const upids = getUpidsFromInstrumentsSampleAreaSelection(currentSelection); 176 const utids = getUtidsFromInstrumentsSampleAreaSelection(currentSelection); 177 if (utids.length === 0 && upids.length === 0) { 178 return undefined; 179 } 180 const metrics = metricsFromTableOrSubquery( 181 ` 182 ( 183 select id, parent_id as parentId, name, self_count 184 from _callstacks_for_callsites!(( 185 select p.callsite_id 186 from instruments_sample p 187 join thread t using (utid) 188 where p.ts >= ${currentSelection.start} 189 and p.ts <= ${currentSelection.end} 190 and ( 191 p.utid in (${utids.join(',')}) 192 or t.upid in (${upids.join(',')}) 193 ) 194 )) 195 ) 196 `, 197 [ 198 { 199 name: 'Instruments Samples', 200 unit: '', 201 columnName: 'self_count', 202 }, 203 ], 204 'include perfetto module appleos.instruments.samples', 205 ); 206 return new QueryFlamegraph(trace, metrics, { 207 state: Flamegraph.createDefaultState(metrics), 208 }); 209} 210 211function getUpidsFromInstrumentsSampleAreaSelection( 212 currentSelection: AreaSelection, 213) { 214 const upids = []; 215 for (const trackInfo of currentSelection.tracks) { 216 if ( 217 trackInfo?.tags?.kind === INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND && 218 trackInfo.tags?.utid === undefined 219 ) { 220 upids.push(assertExists(trackInfo.tags?.upid)); 221 } 222 } 223 return upids; 224} 225 226function getUtidsFromInstrumentsSampleAreaSelection( 227 currentSelection: AreaSelection, 228) { 229 const utids = []; 230 for (const trackInfo of currentSelection.tracks) { 231 if ( 232 trackInfo?.tags?.kind === INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND && 233 trackInfo.tags?.utid !== undefined 234 ) { 235 utids.push(trackInfo.tags?.utid); 236 } 237 } 238 return utids; 239} 240