• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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