1// Copyright (C) 2023 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 {Time, time} from '../../base/time'; 16import {exists} from '../../base/utils'; 17import {openInOldUIWithSizeCheck} from '../../frontend/legacy_trace_viewer'; 18import {Trace} from '../../public/trace'; 19import {App} from '../../public/app'; 20import {PerfettoPlugin} from '../../public/plugin'; 21import { 22 isLegacyTrace, 23 openFileWithLegacyTraceViewer, 24} from '../../frontend/legacy_trace_viewer'; 25import {AppImpl} from '../../core/app_impl'; 26import {addQueryResultsTab} from '../../components/query_table/query_result_tab'; 27import {featureFlags} from '../../core/feature_flags'; 28 29const SQL_STATS = ` 30with first as (select started as ts from sqlstats limit 1) 31select 32 round((max(ended - started, 0))/1e6) as runtime_ms, 33 round((started - first.ts)/1e6) as t_start_ms, 34 query 35from sqlstats, first 36order by started desc`; 37 38const ALL_PROCESSES_QUERY = 'select name, pid from process order by name;'; 39 40const CPU_TIME_FOR_PROCESSES = ` 41select 42 process.name, 43 sum(dur)/1e9 as cpu_sec 44from sched 45join thread using(utid) 46join process using(upid) 47group by upid 48order by cpu_sec desc 49limit 100;`; 50 51const CYCLES_PER_P_STATE_PER_CPU = ` 52select 53 cpu, 54 freq, 55 dur, 56 sum(dur * freq)/1e6 as mcycles 57from ( 58 select 59 cpu, 60 value as freq, 61 lead(ts) over (partition by cpu order by ts) - ts as dur 62 from counter 63 inner join cpu_counter_track on counter.track_id = cpu_counter_track.id 64 where name = 'cpufreq' 65) group by cpu, freq 66order by mcycles desc limit 32;`; 67 68const CPU_TIME_BY_CPU_BY_PROCESS = ` 69select 70 process.name as process, 71 thread.name as thread, 72 cpu, 73 sum(dur) / 1e9 as cpu_sec 74from sched 75inner join thread using(utid) 76inner join process using(upid) 77group by utid, cpu 78order by cpu_sec desc 79limit 30;`; 80 81const HEAP_GRAPH_BYTES_PER_TYPE = ` 82select 83 o.upid, 84 o.graph_sample_ts, 85 c.name, 86 sum(o.self_size) as total_self_size 87from heap_graph_object o join heap_graph_class c on o.type_id = c.id 88group by 89 o.upid, 90 o.graph_sample_ts, 91 c.name 92order by total_self_size desc 93limit 100;`; 94 95const SHOW_OPEN_WITH_LEGACY_UI_BUTTON = featureFlags.register({ 96 id: 'showOpenWithLegacyUiButton', 97 name: 'Show "Open with legacy UI" button', 98 description: 'Show "Open with legacy UI" button in the sidebar', 99 defaultValue: false, 100}); 101 102function getOrPromptForTimestamp(tsRaw: unknown): time | undefined { 103 if (exists(tsRaw)) { 104 if (typeof tsRaw !== 'bigint') { 105 throw Error(`${tsRaw} is not a bigint`); 106 } 107 return Time.fromRaw(tsRaw); 108 } 109 // No args passed, probably run from the command palette. 110 return promptForTimestamp('Enter a timestamp'); 111} 112 113export default class implements PerfettoPlugin { 114 static readonly id = 'perfetto.CoreCommands'; 115 static onActivate(ctx: App) { 116 if (ctx.sidebar.enabled) { 117 ctx.commands.registerCommand({ 118 id: 'perfetto.CoreCommands#ToggleLeftSidebar', 119 name: 'Toggle left sidebar', 120 callback: () => { 121 ctx.sidebar.toggleVisibility(); 122 }, 123 defaultHotkey: '!Mod+B', 124 }); 125 } 126 127 const input = document.createElement('input'); 128 input.classList.add('trace_file'); 129 input.setAttribute('type', 'file'); 130 input.style.display = 'none'; 131 input.addEventListener('change', onInputElementFileSelectionChanged); 132 document.body.appendChild(input); 133 134 const OPEN_TRACE_COMMAND_ID = 'perfetto.CoreCommands#openTrace'; 135 ctx.commands.registerCommand({ 136 id: OPEN_TRACE_COMMAND_ID, 137 name: 'Open trace file', 138 callback: () => { 139 delete input.dataset['useCatapultLegacyUi']; 140 input.click(); 141 }, 142 defaultHotkey: '!Mod+O', 143 }); 144 ctx.sidebar.addMenuItem({ 145 commandId: OPEN_TRACE_COMMAND_ID, 146 section: 'navigation', 147 icon: 'folder_open', 148 }); 149 150 const OPEN_LEGACY_COMMAND_ID = 'perfetto.CoreCommands#openTraceInLegacyUi'; 151 ctx.commands.registerCommand({ 152 id: OPEN_LEGACY_COMMAND_ID, 153 name: 'Open with legacy UI', 154 callback: () => { 155 input.dataset['useCatapultLegacyUi'] = '1'; 156 input.click(); 157 }, 158 }); 159 if (SHOW_OPEN_WITH_LEGACY_UI_BUTTON.get()) { 160 ctx.sidebar.addMenuItem({ 161 commandId: OPEN_LEGACY_COMMAND_ID, 162 section: 'navigation', 163 icon: 'filter_none', 164 }); 165 } 166 } 167 168 async onTraceLoad(ctx: Trace): Promise<void> { 169 ctx.commands.registerCommand({ 170 id: 'perfetto.CoreCommands#RunQueryAllProcesses', 171 name: 'Run query: All processes', 172 callback: () => { 173 addQueryResultsTab(ctx, { 174 query: ALL_PROCESSES_QUERY, 175 title: 'All Processes', 176 }); 177 }, 178 }); 179 180 ctx.commands.registerCommand({ 181 id: 'perfetto.CoreCommands#RunQueryCpuTimeByProcess', 182 name: 'Run query: CPU time by process', 183 callback: () => { 184 addQueryResultsTab(ctx, { 185 query: CPU_TIME_FOR_PROCESSES, 186 title: 'CPU time by process', 187 }); 188 }, 189 }); 190 191 ctx.commands.registerCommand({ 192 id: 'perfetto.CoreCommands#RunQueryCyclesByStateByCpu', 193 name: 'Run query: cycles by p-state by CPU', 194 callback: () => { 195 addQueryResultsTab(ctx, { 196 query: CYCLES_PER_P_STATE_PER_CPU, 197 title: 'Cycles by p-state by CPU', 198 }); 199 }, 200 }); 201 202 ctx.commands.registerCommand({ 203 id: 'perfetto.CoreCommands#RunQueryCyclesByCpuByProcess', 204 name: 'Run query: CPU Time by CPU by process', 205 callback: () => { 206 addQueryResultsTab(ctx, { 207 query: CPU_TIME_BY_CPU_BY_PROCESS, 208 title: 'CPU time by CPU by process', 209 }); 210 }, 211 }); 212 213 ctx.commands.registerCommand({ 214 id: 'perfetto.CoreCommands#RunQueryHeapGraphBytesPerType', 215 name: 'Run query: heap graph bytes per type', 216 callback: () => { 217 addQueryResultsTab(ctx, { 218 query: HEAP_GRAPH_BYTES_PER_TYPE, 219 title: 'Heap graph bytes per type', 220 }); 221 }, 222 }); 223 224 ctx.commands.registerCommand({ 225 id: 'perfetto.CoreCommands#DebugSqlPerformance', 226 name: 'Debug SQL performance', 227 callback: () => { 228 addQueryResultsTab(ctx, { 229 query: SQL_STATS, 230 title: 'Recent SQL queries', 231 }); 232 }, 233 }); 234 235 ctx.commands.registerCommand({ 236 id: 'perfetto.CoreCommands#UnpinAllTracks', 237 name: 'Unpin all pinned tracks', 238 callback: () => { 239 const workspace = ctx.workspace; 240 workspace.pinnedTracks.forEach((t) => workspace.unpinTrack(t)); 241 }, 242 }); 243 244 ctx.commands.registerCommand({ 245 id: 'perfetto.CoreCommands#ExpandAllGroups', 246 name: 'Expand all track groups', 247 callback: () => { 248 ctx.workspace.flatTracks.forEach((track) => track.expand()); 249 }, 250 }); 251 252 ctx.commands.registerCommand({ 253 id: 'perfetto.CoreCommands#CollapseAllGroups', 254 name: 'Collapse all track groups', 255 callback: () => { 256 ctx.workspace.flatTracks.forEach((track) => track.collapse()); 257 }, 258 }); 259 260 ctx.commands.registerCommand({ 261 id: 'perfetto.CoreCommands#PanToTimestamp', 262 name: 'Pan to timestamp', 263 callback: (tsRaw: unknown) => { 264 const ts = getOrPromptForTimestamp(tsRaw); 265 if (ts !== undefined) { 266 ctx.timeline.panToTimestamp(ts); 267 } 268 }, 269 }); 270 271 ctx.commands.registerCommand({ 272 id: 'perfetto.CoreCommands#MarkTimestamp', 273 name: 'Mark timestamp', 274 callback: (tsRaw: unknown) => { 275 const ts = getOrPromptForTimestamp(tsRaw); 276 if (ts !== undefined) { 277 ctx.notes.addNote({ 278 timestamp: ts, 279 }); 280 } 281 }, 282 }); 283 284 ctx.commands.registerCommand({ 285 id: 'perfetto.CoreCommands#ShowCurrentSelectionTab', 286 name: 'Show current selection tab', 287 callback: () => { 288 ctx.tabs.showTab('current_selection'); 289 }, 290 }); 291 292 ctx.commands.registerCommand({ 293 id: 'createNewEmptyWorkspace', 294 name: 'Create new empty workspace', 295 callback: async () => { 296 const workspaces = ctx.workspaces; 297 if (workspaces === undefined) return; // No trace loaded. 298 const name = await ctx.omnibox.prompt('Give it a name...'); 299 if (name === undefined || name === '') return; 300 workspaces.switchWorkspace(workspaces.createEmptyWorkspace(name)); 301 }, 302 }); 303 304 ctx.commands.registerCommand({ 305 id: 'switchWorkspace', 306 name: 'Switch workspace', 307 callback: async () => { 308 const workspaces = ctx.workspaces; 309 if (workspaces === undefined) return; // No trace loaded. 310 const workspace = await ctx.omnibox.prompt('Choose a workspace...', { 311 values: workspaces.all, 312 getName: (ws) => ws.title, 313 }); 314 if (workspace) { 315 workspaces.switchWorkspace(workspace); 316 } 317 }, 318 }); 319 } 320} 321 322function promptForTimestamp(message: string): time | undefined { 323 const tsStr = window.prompt(message); 324 if (tsStr !== null) { 325 try { 326 return Time.fromRaw(BigInt(tsStr)); 327 } catch { 328 window.alert(`${tsStr} is not an integer`); 329 } 330 } 331 return undefined; 332} 333 334function onInputElementFileSelectionChanged(e: Event) { 335 if (!(e.target instanceof HTMLInputElement)) { 336 throw new Error('Not an input element'); 337 } 338 if (!e.target.files) return; 339 const file = e.target.files[0]; 340 // Reset the value so onchange will be fired with the same file. 341 e.target.value = ''; 342 343 if (e.target.dataset['useCatapultLegacyUi'] === '1') { 344 openWithLegacyUi(file); 345 return; 346 } 347 348 AppImpl.instance.analytics.logEvent('Trace Actions', 'Open trace from file'); 349 AppImpl.instance.openTraceFromFile(file); 350} 351 352async function openWithLegacyUi(file: File) { 353 // Switch back to the old catapult UI. 354 AppImpl.instance.analytics.logEvent( 355 'Trace Actions', 356 'Open trace in Legacy UI', 357 ); 358 if (await isLegacyTrace(file)) { 359 return await openFileWithLegacyTraceViewer(file); 360 } 361 return await openInOldUIWithSizeCheck(file); 362} 363