Merge "Enable kgsl ftrace event capture" into main
diff --git a/docs/contributing/ui-plugins.md b/docs/contributing/ui-plugins.md
index 9b1535a..6baadc6 100644
--- a/docs/contributing/ui-plugins.md
+++ b/docs/contributing/ui-plugins.md
@@ -120,7 +120,7 @@
Examples:
- [dev.perfetto.ExampleSimpleCommand](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts).
-- [dev.perfetto.CoreCommands](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.CoreCommands/index.ts).
+- [perfetto.CoreCommands](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/core_plugins/commands/index.ts).
- [dev.perfetto.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleState/index.ts).
#### Hotkeys
diff --git a/src/trace_processor/importers/proto/system_probes_parser.cc b/src/trace_processor/importers/proto/system_probes_parser.cc
index 8f9580f..3cfe982 100644
--- a/src/trace_processor/importers/proto/system_probes_parser.cc
+++ b/src/trace_processor/importers/proto/system_probes_parser.cc
@@ -820,6 +820,10 @@
std::all_of(cpu_infos.begin(), cpu_infos.end(),
[](CpuInfo info) { return info.capacity.has_value(); });
+ bool valid_frequencies =
+ std::all_of(cpu_infos.begin(), cpu_infos.end(),
+ [](CpuInfo info) { return !info.frequencies.empty(); });
+
std::vector<uint32_t> cluster_ids(cpu_infos.size());
uint32_t cluster_id = 0;
@@ -836,7 +840,7 @@
}
cluster_ids[cpu_info.cpu] = cluster_id;
}
- } else {
+ } else if (valid_frequencies) {
// Use max frequency if capacities are invalid
std::vector<CpuMaxFrequency> cpu_max_freqs;
for (CpuInfo& info : cpu_infos) {
diff --git a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
index fcb619a..4a7ccd0 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
@@ -1 +1 @@
-fb0f4cbdc19ce7b744405afcd849e3f4228c910ba055a5050e0c4b35dfe9da79
\ No newline at end of file
+07b0221b35ae6ea9e911040f853e72d19339eec92fd2c439515743ce84dfe985
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
index b2e5b32..1677d33 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
@@ -1 +1 @@
-95e8c1aab784b8503fd4f99d89bdaa0314d0a57ff0d75966c7603afca725e1c5
\ No newline at end of file
+1d86da6fc4d65abe02d0abb376161b7d97c63b0bf7e383353667b84c9f6fcf49
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/stdlib/android/cpu_cluster_tests.py b/test/trace_processor/diff_tests/stdlib/android/cpu_cluster_tests.py
index fc1820b..18e6940 100644
--- a/test/trace_processor/diff_tests/stdlib/android/cpu_cluster_tests.py
+++ b/test/trace_processor/diff_tests/stdlib/android/cpu_cluster_tests.py
@@ -312,3 +312,37 @@
1,1,"[NULL]"
2,2,"[NULL]"
"""))
+
+ def test_android_cpu_cluster_type_no_frequencies(self):
+ return DiffTestBlueprint(
+ trace=TextProto(r"""
+ packet {
+ cpu_info {
+ cpus {
+ processor: "unknown"
+ }
+ cpus {
+ processor: "unknown"
+ }
+ cpus {
+ processor: "unknown"
+ }
+ }
+ }
+ """),
+ query="""
+ INCLUDE PERFETTO MODULE android.cpu.cluster_type;
+
+ SELECT
+ ucpu,
+ cpu,
+ cluster_type
+ FROM
+ android_cpu_cluster_mapping;
+ """,
+ out=Csv("""
+ "ucpu","cpu","cluster_type"
+ 0,0,"[NULL]"
+ 1,1,"[NULL]"
+ 2,2,"[NULL]"
+ """))
diff --git a/ui/src/base/hotkeys.ts b/ui/src/base/hotkeys.ts
index 80672be..2dec16c 100644
--- a/ui/src/base/hotkeys.ts
+++ b/ui/src/base/hotkeys.ts
@@ -46,6 +46,7 @@
// these keys.
import {elementIsEditable} from './dom_utils';
+import {Optional} from './utils';
type Alphabet =
| 'A'
@@ -126,6 +127,34 @@
']',
];
+const macModifierStrings: ReadonlyMap<Modifier, string> = new Map<
+ Modifier,
+ string
+>([
+ ['', ''],
+ ['Mod+', '⌘'],
+ ['Shift+', '⇧'],
+ ['Ctrl+', '⌃'],
+ ['Alt+', '⌥'],
+ ['Mod+Shift+', '⌘⇧'],
+ ['Mod+Alt+', '⌘⌥'],
+ ['Mod+Shift+Alt+', '⌘⇧⌥'],
+ ['Ctrl+Shift+', '⌃⇧'],
+ ['Ctrl+Alt', '⌃⌥'],
+ ['Ctrl+Shift+Alt', '⌃⇧⌥'],
+]);
+
+const pcModifierStrings: ReadonlyMap<Modifier, string> = new Map<
+ Modifier,
+ string
+>([
+ ['', ''],
+ ['Mod+', 'Ctrl+'],
+ ['Mod+Shift+', 'Ctrl+Shift+'],
+ ['Mod+Alt+', 'Ctrl+Alt+'],
+ ['Mod+Shift+Alt+', 'Ctrl+Shift+Alt+'],
+]);
+
// Represents a deconstructed hotkey.
export interface HotkeyParts {
// The name of the primary key of this hotkey.
@@ -141,12 +170,12 @@
// Deconstruct a hotkey from its string representation into its constituent
// parts.
-export function parseHotkey(hotkey: Hotkey): HotkeyParts | null {
+export function parseHotkey(hotkey: Hotkey): Optional<HotkeyParts> {
const regex = /^(!?)((?:Mod\+|Shift\+|Alt\+|Ctrl\+)*)(.*)$/;
const result = hotkey.match(regex);
if (!result) {
- return null;
+ return undefined;
}
return {
@@ -156,6 +185,28 @@
};
}
+// Print the hotkey in a human readable format.
+export function formatHotkey(
+ hotkey: Hotkey,
+ spoof?: Platform,
+): Optional<string> {
+ const parsed = parseHotkey(hotkey);
+ return parsed && formatHotkeyParts(parsed, spoof);
+}
+
+function formatHotkeyParts(
+ {modifier, key}: HotkeyParts,
+ spoof?: Platform,
+): string {
+ return `${formatModifier(modifier, spoof)}${key}`;
+}
+
+function formatModifier(modifier: Modifier, spoof?: Platform): string {
+ const platform = spoof || getPlatform();
+ const strings = platform === 'Mac' ? macModifierStrings : pcModifierStrings;
+ return strings.get(modifier) ?? modifier;
+}
+
// Like |KeyboardEvent| but all fields apart from |key| are optional.
export type KeyboardEventLike = Pick<KeyboardEvent, 'key'> &
Partial<KeyboardEvent>;
@@ -227,10 +278,6 @@
export type Platform = 'Mac' | 'PC';
// Get the current platform (PC or Mac).
-export function getPlatform(spoof?: Platform): Platform {
- if (spoof) {
- return spoof;
- } else {
- return window.navigator.platform.indexOf('Mac') !== -1 ? 'Mac' : 'PC';
- }
+export function getPlatform(): Platform {
+ return window.navigator.platform.indexOf('Mac') !== -1 ? 'Mac' : 'PC';
}
diff --git a/ui/src/base/hotkeys_unittest.ts b/ui/src/base/hotkeys_unittest.ts
index 3ec91ec..08f8297 100644
--- a/ui/src/base/hotkeys_unittest.ts
+++ b/ui/src/base/hotkeys_unittest.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {checkHotkey, Hotkey, parseHotkey} from './hotkeys';
+import {checkHotkey, formatHotkey, Hotkey, parseHotkey} from './hotkeys';
test('parseHotkey', () => {
expect(parseHotkey('A')).toEqual({
@@ -90,3 +90,11 @@
expect(checkHotkey('!X', {key: 'x', target: el})).toBe(true);
});
});
+
+test('formatHotkey', () => {
+ expect(formatHotkey('Mod+X', 'Mac')).toEqual('⌘X');
+ expect(formatHotkey('Mod+Shift+X', 'Mac')).toEqual('⌘⇧X');
+
+ expect(formatHotkey('Mod+X', 'PC')).toEqual('Ctrl+X');
+ expect(formatHotkey('Mod+Shift+X', 'PC')).toEqual('Ctrl+Shift+X');
+});
diff --git a/ui/src/base/registry.ts b/ui/src/base/registry.ts
index 0aa80dc..80f385c 100644
--- a/ui/src/base/registry.ts
+++ b/ui/src/base/registry.ts
@@ -71,6 +71,10 @@
yield* this.registry.values();
}
+ valuesAsArray(): ReadonlyArray<T> {
+ return Array.from(this.values());
+ }
+
unregisterAllForTesting(): void {
this.registry.clear();
}
diff --git a/ui/src/common/commands.ts b/ui/src/common/commands.ts
index a9abcc1..2375559 100644
--- a/ui/src/common/commands.ts
+++ b/ui/src/common/commands.ts
@@ -23,6 +23,10 @@
export class CommandManager {
private readonly registry = new Registry<Command>((cmd) => cmd.id);
+ getCommand(commandId: string): Command {
+ return this.registry.get(commandId);
+ }
+
get commands(): Command[] {
return Array.from(this.registry.values());
}
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index c3d6044..919545d 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -33,6 +33,7 @@
TrackPredicate,
GroupPredicate,
TrackRef,
+ SidebarMenuItem,
} from '../public';
import {EngineBase, Engine} from '../trace_processor/engine';
import {Actions} from './actions';
@@ -55,26 +56,6 @@
private trash = new DisposableStack();
private alive = true;
- readonly sidebar = {
- hide() {
- globals.dispatch(
- Actions.setSidebar({
- visible: false,
- }),
- );
- },
- show() {
- globals.dispatch(
- Actions.setSidebar({
- visible: true,
- }),
- );
- },
- isVisible() {
- return globals.state.sidebarVisible;
- },
- };
-
registerCommand(cmd: Command): void {
// Silently ignore if context is dead.
if (!this.alive) return;
@@ -94,6 +75,10 @@
this.trash.dispose();
this.alive = false;
}
+
+ addSidebarMenuItem(menuItem: SidebarMenuItem): void {
+ this.trash.use(globals.sidebarMenuItems.register(menuItem));
+ }
}
// This PluginContextTrace implementation provides the plugin access to trace
@@ -122,6 +107,13 @@
this.trash.use(dispose);
}
+ addSidebarMenuItem(menuItem: SidebarMenuItem): void {
+ // Silently ignore if context is dead.
+ if (!this.alive) return;
+
+ this.trash.use(globals.sidebarMenuItems.register(menuItem));
+ }
+
registerTrack(trackDesc: TrackDescriptor): void {
// Silently ignore if context is dead.
if (!this.alive) return;
@@ -171,10 +163,6 @@
this.trash.use(unregister);
}
- get sidebar() {
- return this.ctx.sidebar;
- }
-
readonly tabs = {
openQuery: (query: string, title: string) => {
addQueryResultsTab({query, title});
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index b068075..ab57814 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -31,7 +31,6 @@
'dev.perfetto.AndroidPerfTraceCounters',
'dev.perfetto.AndroidStartup',
'dev.perfetto.BookmarkletApi',
- 'dev.perfetto.CoreCommands',
'dev.perfetto.LargeScreensPerf',
'dev.perfetto.PinAndroidPerfMetrics',
'dev.perfetto.PinSysUITracks',
@@ -43,12 +42,14 @@
'perfetto.Annotation',
'perfetto.AsyncSlices',
'perfetto.ChromeScrollJank',
+ 'perfetto.CoreCommands',
'perfetto.Counter',
'perfetto.CpuFreq',
'perfetto.CpuProfile',
'perfetto.CpuSlices',
'perfetto.CriticalUserInteraction',
'perfetto.DebugTracks',
+ 'perfetto.ExampleTraces',
'perfetto.Flows',
'perfetto.Frames',
'perfetto.FtraceRaw',
diff --git a/ui/src/core/query_flamegraph.ts b/ui/src/core/query_flamegraph.ts
index 85cd51c..8e600af 100644
--- a/ui/src/core/query_flamegraph.ts
+++ b/ui/src/core/query_flamegraph.ts
@@ -193,27 +193,27 @@
showStackAndPivot.length === 0
? '0'
: showStackAndPivot
- .map((x, i) => `((name like '%${x}%') << ${i})`)
+ .map((x, i) => `((name like '${makeSqlFilter(x)}') << ${i})`)
.join(' | ');
const showStackBits = (1 << showStackAndPivot.length) - 1;
const hideStackFilter =
hideStack.length === 0
? 'false'
- : hideStack.map((x) => `name like '%${x}%'`).join(' OR ');
+ : hideStack.map((x) => `name like '${makeSqlFilter(x)}'`).join(' OR ');
const showFromFrameFilter =
showFromFrame.length === 0
? '0'
: showFromFrame
- .map((x, i) => `((name like '%${x}%') << ${i})`)
+ .map((x, i) => `((name like '${makeSqlFilter(x)}') << ${i})`)
.join(' | ');
const showFromFrameBits = (1 << showFromFrame.length) - 1;
const hideFrameFilter =
hideFrame.length === 0
? 'false'
- : hideFrame.map((x) => `name like '%${x}%'`).join(' OR ');
+ : hideFrame.map((x) => `name like '${makeSqlFilter(x)}'`).join(' OR ');
const pivotFilter = getPivotFilter(view);
@@ -434,9 +434,16 @@
};
}
+function makeSqlFilter(x: string) {
+ if (x.startsWith('^') && x.endsWith('$')) {
+ return x.slice(1, -1);
+ }
+ return `%${x}%`;
+}
+
function getPivotFilter(view: FlamegraphView) {
if (view.kind === 'PIVOT') {
- return `name like '%${view.pivot}%'`;
+ return `name like '${makeSqlFilter(view.pivot)}'`;
}
if (view.kind === 'BOTTOM_UP') {
return 'value > 0';
diff --git a/ui/src/plugins/dev.perfetto.CoreCommands/index.ts b/ui/src/core_plugins/commands/index.ts
similarity index 60%
rename from ui/src/plugins/dev.perfetto.CoreCommands/index.ts
rename to ui/src/core_plugins/commands/index.ts
index 54cd458..c87284f 100644
--- a/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
+++ b/ui/src/core_plugins/commands/index.ts
@@ -14,12 +14,20 @@
import {Time, time} from '../../base/time';
import {exists} from '../../base/utils';
+import {Actions} from '../../common/actions';
+import {globals} from '../../frontend/globals';
+import {openInOldUIWithSizeCheck} from '../../frontend/legacy_trace_viewer';
import {
Plugin,
PluginContext,
PluginContextTrace,
PluginDescriptor,
} from '../../public';
+import {
+ isLegacyTrace,
+ openFileWithLegacyTraceViewer,
+} from '../../frontend/legacy_trace_viewer';
+import {DisposableStack} from '../../base/disposable_stack';
const SQL_STATS = `
with first as (select started as ts from sqlstats limit 1)
@@ -88,24 +96,76 @@
limit 100;`;
class CoreCommandsPlugin implements Plugin {
+ private readonly disposable = new DisposableStack();
+
onActivate(ctx: PluginContext) {
ctx.registerCommand({
- id: 'dev.perfetto.CoreCommands#ToggleLeftSidebar',
+ id: 'perfetto.CoreCommands#ToggleLeftSidebar',
name: 'Toggle left sidebar',
callback: () => {
- if (ctx.sidebar.isVisible()) {
- ctx.sidebar.hide();
+ if (globals.state.sidebarVisible) {
+ globals.dispatch(
+ Actions.setSidebar({
+ visible: false,
+ }),
+ );
} else {
- ctx.sidebar.show();
+ globals.dispatch(
+ Actions.setSidebar({
+ visible: true,
+ }),
+ );
}
},
defaultHotkey: '!Mod+B',
});
+
+ const input = document.createElement('input');
+ input.classList.add('trace_file');
+ input.setAttribute('type', 'file');
+ input.style.display = 'none';
+ input.addEventListener('change', onInputElementFileSelectionChanged);
+ document.body.appendChild(input);
+ this.disposable.defer(() => {
+ document.body.removeChild(input);
+ });
+
+ const OPEN_TRACE_COMMAND_ID = 'perfetto.CoreCommands#openTrace';
+ ctx.registerCommand({
+ id: OPEN_TRACE_COMMAND_ID,
+ name: 'Open trace file',
+ callback: () => {
+ delete input.dataset['useCatapultLegacyUi'];
+ input.click();
+ },
+ defaultHotkey: '!Mod+O',
+ });
+ ctx.addSidebarMenuItem({
+ commandId: OPEN_TRACE_COMMAND_ID,
+ group: 'navigation',
+ icon: 'folder_open',
+ });
+
+ const OPEN_LEGACY_TRACE_COMMAND_ID =
+ 'perfetto.CoreCommands#openTraceInLegacyUi';
+ ctx.registerCommand({
+ id: OPEN_LEGACY_TRACE_COMMAND_ID,
+ name: 'Open with legacy UI',
+ callback: () => {
+ input.dataset['useCatapultLegacyUi'] = '1';
+ input.click();
+ },
+ });
+ ctx.addSidebarMenuItem({
+ commandId: OPEN_LEGACY_TRACE_COMMAND_ID,
+ group: 'navigation',
+ icon: 'filter_none',
+ });
}
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
ctx.registerCommand({
- id: 'dev.perfetto.CoreCommands#RunQueryAllProcesses',
+ id: 'perfetto.CoreCommands#RunQueryAllProcesses',
name: 'Run query: All processes',
callback: () => {
ctx.tabs.openQuery(ALL_PROCESSES_QUERY, 'All Processes');
@@ -113,7 +173,7 @@
});
ctx.registerCommand({
- id: 'dev.perfetto.CoreCommands#RunQueryCpuTimeByProcess',
+ id: 'perfetto.CoreCommands#RunQueryCpuTimeByProcess',
name: 'Run query: CPU time by process',
callback: () => {
ctx.tabs.openQuery(CPU_TIME_FOR_PROCESSES, 'CPU time by process');
@@ -121,7 +181,7 @@
});
ctx.registerCommand({
- id: 'dev.perfetto.CoreCommands#RunQueryCyclesByStateByCpu',
+ id: 'perfetto.CoreCommands#RunQueryCyclesByStateByCpu',
name: 'Run query: cycles by p-state by CPU',
callback: () => {
ctx.tabs.openQuery(
@@ -132,7 +192,7 @@
});
ctx.registerCommand({
- id: 'dev.perfetto.CoreCommands#RunQueryCyclesByCpuByProcess',
+ id: 'perfetto.CoreCommands#RunQueryCyclesByCpuByProcess',
name: 'Run query: CPU Time by CPU by process',
callback: () => {
ctx.tabs.openQuery(
@@ -143,7 +203,7 @@
});
ctx.registerCommand({
- id: 'dev.perfetto.CoreCommands#RunQueryHeapGraphBytesPerType',
+ id: 'perfetto.CoreCommands#RunQueryHeapGraphBytesPerType',
name: 'Run query: heap graph bytes per type',
callback: () => {
ctx.tabs.openQuery(
@@ -154,7 +214,7 @@
});
ctx.registerCommand({
- id: 'dev.perfetto.CoreCommands#DebugSqlPerformance',
+ id: 'perfetto.CoreCommands#DebugSqlPerformance',
name: 'Debug SQL performance',
callback: () => {
ctx.tabs.openQuery(SQL_STATS, 'Recent SQL queries');
@@ -162,7 +222,7 @@
});
ctx.registerCommand({
- id: 'dev.perfetto.CoreCommands#UnpinAllTracks',
+ id: 'perfetto.CoreCommands#UnpinAllTracks',
name: 'Unpin all pinned tracks',
callback: () => {
ctx.timeline.unpinTracksByPredicate((_) => {
@@ -172,7 +232,7 @@
});
ctx.registerCommand({
- id: 'dev.perfetto.CoreCommands#ExpandAllGroups',
+ id: 'perfetto.CoreCommands#ExpandAllGroups',
name: 'Expand all track groups',
callback: () => {
ctx.timeline.expandGroupsByPredicate((_) => {
@@ -182,7 +242,7 @@
});
ctx.registerCommand({
- id: 'dev.perfetto.CoreCommands#CollapseAllGroups',
+ id: 'perfetto.CoreCommands#CollapseAllGroups',
name: 'Collapse all track groups',
callback: () => {
ctx.timeline.collapseGroupsByPredicate((_) => {
@@ -192,7 +252,7 @@
});
ctx.registerCommand({
- id: 'dev.perfetto.CoreCommands#PanToTimestamp',
+ id: 'perfetto.CoreCommands#PanToTimestamp',
name: 'Pan to timestamp',
callback: (tsRaw: unknown) => {
if (exists(tsRaw)) {
@@ -211,13 +271,17 @@
});
ctx.registerCommand({
- id: 'dev.perfetto.CoreCommands#ShowCurrentSelectionTab',
+ id: 'perfetto.CoreCommands#ShowCurrentSelectionTab',
name: 'Show current selection tab',
callback: () => {
ctx.tabs.showTab('current_selection');
},
});
}
+
+ onDeactivate(_: PluginContext): void {
+ this.disposable[Symbol.dispose]();
+ }
}
function promptForTimestamp(message: string): time | undefined {
@@ -232,7 +296,35 @@
return undefined;
}
+function onInputElementFileSelectionChanged(e: Event) {
+ if (!(e.target instanceof HTMLInputElement)) {
+ throw new Error('Not an input element');
+ }
+ if (!e.target.files) return;
+ const file = e.target.files[0];
+ // Reset the value so onchange will be fired with the same file.
+ e.target.value = '';
+
+ if (e.target.dataset['useCatapultLegacyUi'] === '1') {
+ openWithLegacyUi(file);
+ return;
+ }
+
+ globals.logging.logEvent('Trace Actions', 'Open trace from file');
+ globals.dispatch(Actions.openTraceFromFile({file}));
+}
+
+async function openWithLegacyUi(file: File) {
+ // Switch back to the old catapult UI.
+ globals.logging.logEvent('Trace Actions', 'Open trace in Legacy UI');
+ if (await isLegacyTrace(file)) {
+ openFileWithLegacyTraceViewer(file);
+ return;
+ }
+ openInOldUIWithSizeCheck(file);
+}
+
export const plugin: PluginDescriptor = {
- pluginId: 'dev.perfetto.CoreCommands',
+ pluginId: 'perfetto.CoreCommands',
plugin: CoreCommandsPlugin,
};
diff --git a/ui/src/core_plugins/example_traces/index.ts b/ui/src/core_plugins/example_traces/index.ts
new file mode 100644
index 0000000..d1fc43d
--- /dev/null
+++ b/ui/src/core_plugins/example_traces/index.ts
@@ -0,0 +1,67 @@
+// Copyright (C) 2024 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 {Actions} from '../../common/actions';
+import {globals} from '../../frontend/globals';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
+
+const EXAMPLE_ANDROID_TRACE_URL =
+ 'https://storage.googleapis.com/perfetto-misc/example_android_trace_15s';
+
+const EXAMPLE_CHROME_TRACE_URL =
+ 'https://storage.googleapis.com/perfetto-misc/chrome_example_wikipedia.perfetto_trace.gz';
+
+function openTraceUrl(url: string): void {
+ globals.logging.logEvent('Trace Actions', 'Open example trace');
+ globals.dispatch(Actions.openTraceFromUrl({url}));
+}
+
+class ExampleTracesPlugin implements Plugin {
+ onActivate(ctx: PluginContext) {
+ const OPEN_EXAMPLE_ANDROID_TRACE_COMMAND_ID =
+ 'perfetto.CoreCommands#openExampleAndroidTrace';
+ ctx.registerCommand({
+ id: OPEN_EXAMPLE_ANDROID_TRACE_COMMAND_ID,
+ name: 'Open Android example',
+ callback: () => {
+ openTraceUrl(EXAMPLE_ANDROID_TRACE_URL);
+ },
+ });
+ ctx.addSidebarMenuItem({
+ commandId: OPEN_EXAMPLE_ANDROID_TRACE_COMMAND_ID,
+ group: 'example_traces',
+ icon: 'description',
+ });
+
+ const OPEN_EXAMPLE_CHROME_TRACE_COMMAND_ID =
+ 'perfetto.CoreCommands#openExampleChromeTrace';
+ ctx.registerCommand({
+ id: OPEN_EXAMPLE_CHROME_TRACE_COMMAND_ID,
+ name: 'Open Chrome example',
+ callback: () => {
+ openTraceUrl(EXAMPLE_CHROME_TRACE_URL);
+ },
+ });
+ ctx.addSidebarMenuItem({
+ commandId: OPEN_EXAMPLE_CHROME_TRACE_COMMAND_ID,
+ group: 'example_traces',
+ icon: 'description',
+ });
+ }
+}
+
+export const plugin: PluginDescriptor = {
+ pluginId: 'perfetto.ExampleTraces',
+ plugin: ExampleTracesPlugin,
+};
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index e9c91a7..f6d3939 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -364,7 +364,6 @@
id: 'perfetto.RunQuery',
name: 'Run query',
callback: () => globals.omnibox.setMode(OmniboxMode.Query),
- defaultHotkey: '!Mod+O',
},
{
id: 'perfetto.Search',
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index ce10df2..f497a6b 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -51,6 +51,8 @@
} from './search_overview_track';
import {AppContext} from './app_context';
import {TraceContext} from './trace_context';
+import {Registry} from '../base/registry';
+import {SidebarMenuItem} from '../public';
const INSTANT_FOCUS_DURATION = 1n;
const INCOMPLETE_SLICE_DURATION = 30_000n;
@@ -237,6 +239,8 @@
traceContext = defaultTraceContext;
+ readonly sidebarMenuItems = new Registry<SidebarMenuItem>((m) => m.commandId);
+
// This is the app's equivalent of a plugin's onTraceLoad() function.
// TODO(stevegolton): Eventually initialization that should be done on trace
// load should be moved into here, and then we can remove TraceController
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 8efff31..c8a078b 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -264,8 +264,6 @@
if (globals.testing) {
document.body.classList.add('testing');
}
-
- pluginManager.initialize();
}
function onCssLoaded() {
@@ -355,6 +353,12 @@
// cases.
routeChange(route);
});
+
+ // Force one initial render to get everything in place
+ m.render(document.body, m(App, router.resolve()));
+
+ // Initialize plugins, now that we are ready to go
+ pluginManager.initialize();
}
// If the URL is /#!?rpc_port=1234, change the default RPC port.
diff --git a/ui/src/frontend/legacy_trace_viewer.ts b/ui/src/frontend/legacy_trace_viewer.ts
index f12e370..f496712 100644
--- a/ui/src/frontend/legacy_trace_viewer.ts
+++ b/ui/src/frontend/legacy_trace_viewer.ts
@@ -19,6 +19,7 @@
import {showModal} from '../widgets/modal';
import {globals} from './globals';
import {utf8Decode} from '../base/string_utils';
+import {convertToJson} from './trace_converter';
const CTRACE_HEADER = 'TRACE:\n';
@@ -171,6 +172,56 @@
});
}
+export function openInOldUIWithSizeCheck(trace: Blob) {
+ // Perfetto traces smaller than 50mb can be safely opened in the legacy UI.
+ if (trace.size < 1024 * 1024 * 50) {
+ convertToJson(trace);
+ return;
+ }
+
+ // Give the user the option to truncate larger perfetto traces.
+ const size = Math.round(trace.size / (1024 * 1024));
+ showModal({
+ title: 'Legacy UI may fail to open this trace',
+ content: m(
+ 'div',
+ m(
+ 'p',
+ `This trace is ${size}mb, opening it in the legacy UI ` + `may fail.`,
+ ),
+ m(
+ 'p',
+ 'More options can be found at ',
+ m(
+ 'a',
+ {
+ href: 'https://goto.google.com/opening-large-traces',
+ target: '_blank',
+ },
+ 'go/opening-large-traces',
+ ),
+ '.',
+ ),
+ ),
+ buttons: [
+ {
+ text: 'Open full trace (not recommended)',
+ action: () => convertToJson(trace),
+ },
+ {
+ text: 'Open beginning of trace',
+ action: () => convertToJson(trace, /* truncate*/ 'start'),
+ },
+ {
+ text: 'Open end of trace',
+ primary: true,
+ action: () => convertToJson(trace, /* truncate*/ 'end'),
+ },
+ ],
+ });
+ return;
+}
+
// TraceViewer method that we wire up to trigger the file load.
interface TraceViewerAPI extends Element {
setActiveTrace(name: string, data: ArrayBuffer | string): void;
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index c458baa..c83ce11 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -98,11 +98,11 @@
e.preventDefault();
if (allCollapsed) {
globals.commandManager.runCommand(
- 'dev.perfetto.CoreCommands#ExpandAllGroups',
+ 'perfetto.CoreCommands#ExpandAllGroups',
);
} else {
globals.commandManager.runCommand(
- 'dev.perfetto.CoreCommands#CollapseAllGroups',
+ 'perfetto.CoreCommands#CollapseAllGroups',
);
}
},
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index b1599ab..02a42c7 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -36,17 +36,15 @@
import {downloadData, downloadUrl} from './download_utils';
import {globals} from './globals';
import {toggleHelp} from './help_modal';
-import {
- isLegacyTrace,
- openFileWithLegacyTraceViewer,
-} from './legacy_trace_viewer';
import {Router} from './router';
import {createTraceLink, isDownloadable, shareTrace} from './trace_attrs';
import {
- convertToJson,
convertTraceToJsonAndDownload,
convertTraceToSystraceAndDownload,
} from './trace_converter';
+import {openInOldUIWithSizeCheck} from './legacy_trace_viewer';
+import {formatHotkey} from '../base/hotkeys';
+import {SidebarMenuItem} from '../public';
const GITILES_URL =
'https://android.googlesource.com/platform/external/perfetto';
@@ -98,16 +96,11 @@
return globals.isInternalUser && HIRING_BANNER_FLAG.get();
}
-export const EXAMPLE_ANDROID_TRACE_URL =
- 'https://storage.googleapis.com/perfetto-misc/example_android_trace_15s';
-
-export const EXAMPLE_CHROME_TRACE_URL =
- 'https://storage.googleapis.com/perfetto-misc/chrome_example_wikipedia.perfetto_trace.gz';
-
interface SectionItem {
t: string;
a: string | ((e: Event) => void);
i: string;
+ title?: string;
isPending?: () => boolean;
isVisible?: () => boolean;
internalUserOnly?: boolean;
@@ -125,6 +118,34 @@
appendOpenedTraceTitle?: boolean;
}
+function insertSidebarMenuitems(
+ groupSelector: SidebarMenuItem['group'],
+): ReadonlyArray<SectionItem> {
+ return globals.sidebarMenuItems
+ .valuesAsArray()
+ .filter(({group}) => group === groupSelector)
+ .sort((a, b) => {
+ const prioA = a.priority ?? 0;
+ const prioB = b.priority ?? 0;
+ return prioA - prioB;
+ })
+ .map((item) => {
+ const cmd = globals.commandManager.getCommand(item.commandId);
+ const title = cmd.defaultHotkey
+ ? `${cmd.name} [${formatHotkey(cmd.defaultHotkey)}]`
+ : cmd.name;
+ return {
+ t: cmd.name,
+ a: (e: Event) => {
+ e.preventDefault();
+ cmd.callback();
+ },
+ i: item.icon,
+ title,
+ };
+ });
+}
+
function getSections(): Section[] {
return [
{
@@ -132,12 +153,7 @@
summary: 'Open or record a new trace',
expanded: true,
items: [
- {t: 'Open trace file', a: popupFileSelectionDialog, i: 'folder_open'},
- {
- t: 'Open with legacy UI',
- a: popupFileSelectionDialogOldUI,
- i: 'filter_none',
- },
+ ...insertSidebarMenuitems('navigation'),
{t: 'Record new trace', a: navigateRecord, i: 'fiber_smart_record'},
{
t: 'Widgets',
@@ -236,18 +252,7 @@
title: 'Example Traces',
expanded: true,
summary: 'Open an example trace',
- items: [
- {
- t: 'Open Android example',
- a: openTraceUrl(EXAMPLE_ANDROID_TRACE_URL),
- i: 'description',
- },
- {
- t: 'Open Chrome example',
- a: openTraceUrl(EXAMPLE_CHROME_TRACE_URL),
- i: 'description',
- },
- ],
+ items: [...insertSidebarMenuitems('example_traces')],
},
{
@@ -285,24 +290,6 @@
toggleHelp();
}
-function getFileElement(): HTMLInputElement {
- return assertExists(
- document.querySelector<HTMLInputElement>('input[type=file]'),
- );
-}
-
-function popupFileSelectionDialog(e: Event) {
- e.preventDefault();
- delete getFileElement().dataset['useCatapultLegacyUi'];
- getFileElement().click();
-}
-
-function popupFileSelectionDialogOldUI(e: Event) {
- e.preventDefault();
- getFileElement().dataset['useCatapultLegacyUi'] = '1';
- getFileElement().click();
-}
-
function downloadTraceFromUrl(url: string): Promise<File> {
return m.request({
method: 'GET',
@@ -388,92 +375,6 @@
return globals.getCurrentEngine() !== undefined;
}
-export function openTraceUrl(url: string): (e: Event) => void {
- return (e) => {
- globals.logging.logEvent('Trace Actions', 'Open example trace');
- e.preventDefault();
- globals.dispatch(Actions.openTraceFromUrl({url}));
- };
-}
-
-function onInputElementFileSelectionChanged(e: Event) {
- if (!(e.target instanceof HTMLInputElement)) {
- throw new Error('Not an input element');
- }
- if (!e.target.files) return;
- const file = e.target.files[0];
- // Reset the value so onchange will be fired with the same file.
- e.target.value = '';
-
- if (e.target.dataset['useCatapultLegacyUi'] === '1') {
- openWithLegacyUi(file);
- return;
- }
-
- globals.logging.logEvent('Trace Actions', 'Open trace from file');
- globals.dispatch(Actions.openTraceFromFile({file}));
-}
-
-async function openWithLegacyUi(file: File) {
- // Switch back to the old catapult UI.
- globals.logging.logEvent('Trace Actions', 'Open trace in Legacy UI');
- if (await isLegacyTrace(file)) {
- openFileWithLegacyTraceViewer(file);
- return;
- }
- openInOldUIWithSizeCheck(file);
-}
-
-function openInOldUIWithSizeCheck(trace: Blob) {
- // Perfetto traces smaller than 50mb can be safely opened in the legacy UI.
- if (trace.size < 1024 * 1024 * 50) {
- convertToJson(trace);
- return;
- }
-
- // Give the user the option to truncate larger perfetto traces.
- const size = Math.round(trace.size / (1024 * 1024));
- showModal({
- title: 'Legacy UI may fail to open this trace',
- content: m(
- 'div',
- m(
- 'p',
- `This trace is ${size}mb, opening it in the legacy UI ` + `may fail.`,
- ),
- m(
- 'p',
- 'More options can be found at ',
- m(
- 'a',
- {
- href: 'https://goto.google.com/opening-large-traces',
- target: '_blank',
- },
- 'go/opening-large-traces',
- ),
- '.',
- ),
- ),
- buttons: [
- {
- text: 'Open full trace (not recommended)',
- action: () => convertToJson(trace),
- },
- {
- text: 'Open beginning of trace',
- action: () => convertToJson(trace, /* truncate*/ 'start'),
- },
- {
- text: 'Open end of trace',
- primary: true,
- action: () => convertToJson(trace, /* truncate*/ 'end'),
- },
- ],
- });
- return;
-}
-
function navigateRecord(e: Event) {
e.preventDefault();
Router.navigate('#!/record');
@@ -866,7 +767,15 @@
};
}
vdomItems.push(
- m('li', m(`a${css}`, attrs, m('i.material-icons', item.i), item.t)),
+ m(
+ 'li',
+ m(
+ `a${css}`,
+ {...attrs, title: item.title},
+ m('i.material-icons', item.i),
+ item.t,
+ ),
+ ),
);
}
if (section.appendOpenedTraceTitle) {
@@ -917,7 +826,7 @@
{
onclick: () => {
globals.commandManager.runCommand(
- 'dev.perfetto.CoreCommands#ToggleLeftSidebar',
+ 'perfetto.CoreCommands#ToggleLeftSidebar',
);
},
},
@@ -930,9 +839,6 @@
),
),
),
- m('input.trace_file[type=file]', {
- onchange: onInputElementFileSelectionChanged,
- }),
m(
'.sidebar-scroll',
m('.sidebar-scroll-container', ...vdomSections, m(SidebarFooter)),
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index e73b32d..730ea90 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -133,6 +133,19 @@
path: string[];
}
+export interface SidebarMenuItem {
+ readonly commandId: string;
+ readonly group:
+ | 'navigation'
+ | 'current_trace'
+ | 'convert_trace'
+ | 'example_traces'
+ | 'support';
+ when?(): boolean;
+ readonly icon: string;
+ readonly priority?: number;
+}
+
// This interface defines a context for a plugin, which is an object passed to
// most hooks within the plugin. It should be used to interact with Perfetto.
export interface PluginContext {
@@ -146,17 +159,10 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
runCommand(id: string, ...args: any[]): any;
- // Control of the sidebar.
- sidebar: {
- // Show the sidebar.
- show(): void;
-
- // Hide the sidebar.
- hide(): void;
-
- // Returns true if the sidebar is visible.
- isVisible(): boolean;
- };
+ // Adds a new menu item to the sidebar.
+ // All entries must map to a command. This will allow the shortcut and
+ // optional shortcut to be displayed on the UI.
+ addSidebarMenuItem(menuItem: SidebarMenuItem): void;
}
export interface SliceTrackColNames {
diff --git a/ui/src/widgets/flamegraph.ts b/ui/src/widgets/flamegraph.ts
index e60952b..22722f6 100644
--- a/ui/src/widgets/flamegraph.ts
+++ b/ui/src/widgets/flamegraph.ts
@@ -17,7 +17,6 @@
import {findRef} from '../base/dom_utils';
import {assertExists, assertTrue} from '../base/logging';
import {Monitor} from '../base/monitor';
-import {cropText} from '../base/string_utils';
import {Button, ButtonBar} from './button';
import {EmptyState} from './empty_state';
@@ -28,7 +27,7 @@
import {TagInput} from './tag_input';
import {SegmentedButtons} from './segmented_buttons';
-const LABEL_FONT_STYLE = '12px Roboto Mono';
+const LABEL_FONT_STYLE = '12px Roboto';
const NODE_HEIGHT = 20;
const MIN_PIXEL_DISPLAYED = 3;
const FILTER_COMMON_TEXT = `
@@ -456,7 +455,7 @@
if (widthNoPadding >= LABEL_MIN_WIDTH_FOR_TEXT_PX) {
ctx.fillStyle = 'black';
ctx.fillText(
- cropText(name, this.labelCharWidth, widthNoPadding),
+ name.substring(0, widthNoPadding / this.labelCharWidth),
x + LABEL_PADDING_PX,
y + (NODE_HEIGHT - 1) / 2,
widthNoPadding,
@@ -639,31 +638,31 @@
m(Button, {
label: 'Show Stack',
onclick: () => {
- filterButtonClick(`Show Stack: ${name}`);
+ filterButtonClick(`Show Stack: ^${name}$`);
},
}),
m(Button, {
label: 'Hide Stack',
onclick: () => {
- filterButtonClick(`Hide Stack: ${name}`);
+ filterButtonClick(`Hide Stack: ^${name}$`);
},
}),
m(Button, {
label: 'Hide Frame',
onclick: () => {
- filterButtonClick(`Hide Frame: ${name}`);
+ filterButtonClick(`Hide Frame: ^${name}$`);
},
}),
m(Button, {
label: 'Show From Frame',
onclick: () => {
- filterButtonClick(`Show From Frame: ${name}`);
+ filterButtonClick(`Show From Frame: ^${name}$`);
},
}),
m(Button, {
label: 'Pivot',
onclick: () => {
- filterButtonClick(`Pivot: ${name}`);
+ filterButtonClick(`Pivot: ^${name}$`);
},
}),
),
diff --git a/ui/src/widgets/hotkey_glyphs.ts b/ui/src/widgets/hotkey_glyphs.ts
index 667410c..90ac6d4 100644
--- a/ui/src/widgets/hotkey_glyphs.ts
+++ b/ui/src/widgets/hotkey_glyphs.ts
@@ -28,7 +28,7 @@
view({attrs}: m.Vnode<HotkeyGlyphsAttrs>) {
const {hotkey, spoof} = attrs;
- const platform = getPlatform(spoof);
+ const platform = spoof || getPlatform();
const result = parseHotkey(hotkey);
if (result) {
const {key, modifier} = result;
@@ -60,7 +60,7 @@
export class KeycapGlyph implements m.ClassComponent<KeycapGlyphsAttrs> {
view({attrs}: m.Vnode<KeycapGlyphsAttrs>) {
const {keyValue, spoof} = attrs;
- const platform = getPlatform(spoof);
+ const platform = spoof || getPlatform();
return m('span.pf-keycap', glyphForKey(keyValue, platform));
}
}