[ui] Tweaked plugin API and added more functionality to timeline API.
- Merge `Viewer` into `PluginContext` and `PluginContextTrace`.
- Re-jig naming conventions.
- Add extra methods to control the timeline.
Change-Id: I780c00a824c205fdd32441313a9c6c47f7c83664
diff --git a/docs/contributing/ui-plugins.md b/docs/contributing/ui-plugins.md
index 23fbe25..3b84547 100644
--- a/docs/contributing/ui-plugins.md
+++ b/docs/contributing/ui-plugins.md
@@ -64,8 +64,8 @@
Follow the [create a plugin](#create-a-plugin) to get an initial
skeleton for your plugin.
-To add your first command, add a call to `ctx.addCommand()` in either your
-`onActivate()` or `onTraceLoad()` hooks. The recommendation is to register
+To add your first command, add a call to `ctx.registerCommand()` in either
+your `onActivate()` or `onTraceLoad()` hooks. The recommendation is to register
commands in `onActivate()` by default unless they require something from
`TracePluginContext` which is not available on `PluginContext`.
@@ -76,7 +76,7 @@
```typescript
class MyPlugin implements Plugin {
onActivate(ctx: PluginContext): void {
- ctx.addCommand(
+ ctx.registerCommand(
{
id: 'dev.perfetto.ExampleSimpleCommand#LogHelloPlugin',
name: 'Log "Hello, plugin!"',
@@ -86,7 +86,7 @@
}
onTraceLoad(ctx: TracePluginContext): void {
- ctx.addCommand(
+ ctx.registerCommand(
{
id: 'dev.perfetto.ExampleSimpleTraceCommand#LogHelloTrace',
name: 'Log "Hello, trace!"',
diff --git a/python/tools/check_ratchet.py b/python/tools/check_ratchet.py
index ab6a5b7..2dc27ac 100755
--- a/python/tools/check_ratchet.py
+++ b/python/tools/check_ratchet.py
@@ -36,7 +36,7 @@
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
UI_SRC_DIR = os.path.join(ROOT_DIR, 'ui', 'src')
-EXPECTED_ANY_COUNT = 74
+EXPECTED_ANY_COUNT = 73
# 'any' is too generic. It will show up in many comments etc. So
# instead of counting any directly we forbid it using eslint and count
# the number of suppressions.
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 091aacb..69e0390 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {v4 as uuidv4} from 'uuid';
+
import {Disposable, Trash} from '../base/disposable';
import {assertFalse} from '../base/logging';
-import {ViewerImpl, ViewerProxy} from '../common/viewer';
import {globals} from '../frontend/globals';
import {
Command,
@@ -26,16 +27,19 @@
PluginContext,
PluginContextTrace,
PluginDescriptor,
+ PrimaryTrackSortKey,
Store,
Track,
TrackContext,
TrackDescriptor,
+ TrackPredicate,
TrackRef,
- Viewer,
} from '../public';
import {Engine} from '../trace_processor/engine';
+import {Actions} from './actions';
import {Registry} from './registry';
+import {SCROLLING_TRACK_GROUP} from './state';
// Every plugin gets its own PluginContext. This is how we keep track
// what each plugin is doing and how we can blame issues on particular
@@ -45,13 +49,23 @@
private trash = new Trash();
private alive = true;
- constructor(
- readonly pluginId: string, readonly viewer: ViewerProxy,
- private commandRegistry: Map<string, Command>) {
- this.trash.add(viewer);
- }
+ readonly sidebar = {
+ hide() {
+ globals.dispatch(Actions.setSidebar({
+ visible: false,
+ }));
+ },
+ show() {
+ globals.dispatch(Actions.setSidebar({
+ visible: true,
+ }));
+ },
+ isVisible() {
+ return globals.state.sidebarVisible;
+ },
+ };
- addCommand(cmd: Command): void {
+ registerCommand(cmd: Command): void {
// Silently ignore if context is dead.
if (!this.alive) return;
@@ -64,8 +78,21 @@
this.commandRegistry.delete(id);
},
});
+ };
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ runCommand(id: string, ...args: any[]): any {
+ return globals.commandManager.runCommand(id, ...args);
+ };
+
+ get commands(): Command[] {
+ return globals.commandManager.commands;
}
+ constructor(
+ readonly pluginId: string,
+ private commandRegistry: Map<string, Command>) {}
+
dispose(): void {
this.trash.dispose();
this.alive = false;
@@ -88,7 +115,7 @@
this.trash.add(engine);
}
- addCommand(cmd: Command): void {
+ registerCommand(cmd: Command): void {
// Silently ignore if context is dead.
if (!this.alive) return;
@@ -103,29 +130,14 @@
});
}
- get viewer(): Viewer {
- return this.ctx.viewer;
- }
-
- get pluginId(): string {
- return this.ctx.pluginId;
- }
-
- // Register a new track in this context.
- // All tracks registered through this method are removed when this context is
- // destroyed, i.e. when the trace is unloaded.
registerTrack(trackDesc: TrackDescriptor): void {
// Silently ignore if context is dead.
if (!this.alive) return;
-
this.trackRegistry.set(trackDesc.uri, trackDesc);
this.trash.addCallback(() => this.trackRegistry.delete(trackDesc.uri));
}
addDefaultTrack(track: TrackRef): void {
- // Silently ignore if context is dead.
- if (!this.alive) return;
-
this.defaultTracks.add(track);
this.trash.addCallback(() => this.defaultTracks.delete(track));
}
@@ -142,6 +154,112 @@
// });
}
+ get commands(): Command[] {
+ return this.ctx.commands;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ runCommand(id: string, ...args: any[]): any {
+ return this.ctx.runCommand(id, ...args);
+ }
+
+ get sidebar() {
+ return this.ctx.sidebar;
+ }
+
+ readonly tabs = {
+ openQuery: globals.openQuery,
+ };
+
+ get pluginId(): string {
+ return this.ctx.pluginId;
+ }
+
+ readonly timeline = {
+ // Add a new track to the timeline, returning its key.
+ addTrack(uri: string, displayName: string, params?: unknown): string {
+ const trackKey = uuidv4();
+ globals.dispatch(Actions.addTrack({
+ key: trackKey,
+ uri,
+ name: displayName,
+ params,
+ trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
+ trackGroup: SCROLLING_TRACK_GROUP,
+ }));
+ return trackKey;
+ },
+
+ removeTrack(key: string):
+ void {
+ globals.dispatch(Actions.removeTracks({trackKeys: [key]}));
+ },
+
+ pinTrack(key: string) {
+ if (!isPinned(key)) {
+ globals.dispatch(Actions.toggleTrackPinned({trackKey: key}));
+ }
+ },
+
+ unpinTrack(key: string) {
+ if (isPinned(key)) {
+ globals.dispatch(Actions.toggleTrackPinned({trackKey: key}));
+ }
+ },
+
+ pinTracksByPredicate(predicate: TrackPredicate) {
+ const tracks = Object.values(globals.state.tracks);
+ for (const track of tracks) {
+ const tags = {
+ name: track.name,
+ };
+ if (predicate(tags) && !isPinned(track.key)) {
+ globals.dispatch(Actions.toggleTrackPinned({
+ trackKey: track.key,
+ }));
+ }
+ }
+ },
+
+ unpinTracksByPredicate(predicate: TrackPredicate) {
+ const tracks = Object.values(globals.state.tracks);
+ for (const track of tracks) {
+ const tags = {
+ name: track.name,
+ };
+ if (predicate(tags) && isPinned(track.key)) {
+ globals.dispatch(Actions.toggleTrackPinned({
+ trackKey: track.key,
+ }));
+ }
+ }
+ },
+
+ removeTracksByPredicate(predicate: TrackPredicate) {
+ const trackKeysToRemove = Object.values(globals.state.tracks)
+ .filter((track) => {
+ const tags = {
+ name: track.name,
+ };
+ return predicate(tags);
+ })
+ .map((trackState) => trackState.key);
+
+ globals.dispatch(Actions.removeTracks({trackKeys: trackKeysToRemove}));
+ },
+
+ get tracks():
+ TrackRef[] {
+ return Object.values(globals.state.tracks).map((trackState) => {
+ return {
+ displayName: trackState.name,
+ uri: trackState.uri,
+ params: trackState.params,
+ };
+ });
+ },
+ };
+
dispose(): void {
this.trash.dispose();
this.alive = false;
@@ -164,6 +282,10 @@
}
}
+function isPinned(trackId: string): boolean {
+ return globals.state.pinnedTracks.includes(trackId);
+}
+
// 'Static' registry of all known plugins.
export class PluginRegistry extends Registry<PluginDescriptor> {
constructor() {
@@ -209,7 +331,7 @@
this.plugins = new Map();
}
- activatePlugin(id: string, viewer: ViewerImpl): void {
+ activatePlugin(id: string): void {
if (this.isActive(id)) {
return;
}
@@ -217,9 +339,7 @@
const pluginInfo = this.registry.get(id);
const plugin = makePlugin(pluginInfo);
- const viewerProxy = viewer.getProxy(id);
- const context =
- new PluginContextImpl(id, viewerProxy, this.commandRegistry);
+ const context = new PluginContextImpl(id, this.commandRegistry);
plugin.onActivate && plugin.onActivate(context);
diff --git a/ui/src/common/plugins_unittest.ts b/ui/src/common/plugins_unittest.ts
index 1d17350..05238a5 100644
--- a/ui/src/common/plugins_unittest.ts
+++ b/ui/src/common/plugins_unittest.ts
@@ -18,7 +18,6 @@
import {createEmptyState} from './empty_state';
import {PluginManager, PluginRegistry} from './plugins';
-import {ViewerImpl} from './viewer';
class FakeEngine extends Engine {
id: string = 'TestEngine';
@@ -35,7 +34,6 @@
};
}
-const viewer = new ViewerImpl();
const engine = new FakeEngine();
globals.initStore(createEmptyState());
@@ -57,14 +55,14 @@
});
it('can activate plugin', () => {
- manager.activatePlugin('foo', viewer);
+ manager.activatePlugin('foo');
expect(manager.isActive('foo')).toBe(true);
expect(mockPlugin.onActivate).toHaveBeenCalledTimes(1);
});
it('can deactivate plugin', () => {
- manager.activatePlugin('foo', viewer);
+ manager.activatePlugin('foo');
manager.deactivatePlugin('foo');
expect(manager.isActive('foo')).toBe(false);
@@ -72,7 +70,7 @@
});
it('invokes onTraceLoad when trace is loaded', () => {
- manager.activatePlugin('foo', viewer);
+ manager.activatePlugin('foo');
manager.onTraceLoad(engine);
expect(mockPlugin.onTraceLoad).toHaveBeenCalledTimes(1);
@@ -80,13 +78,13 @@
it('invokes onTraceLoad when plugin activated while trace loaded', () => {
manager.onTraceLoad(engine);
- manager.activatePlugin('foo', viewer);
+ manager.activatePlugin('foo');
expect(mockPlugin.onTraceLoad).toHaveBeenCalledTimes(1);
});
it('invokes onTraceUnload when plugin deactivated while trace loaded', () => {
- manager.activatePlugin('foo', viewer);
+ manager.activatePlugin('foo');
manager.onTraceLoad(engine);
manager.deactivatePlugin('foo');
diff --git a/ui/src/common/viewer.ts b/ui/src/common/viewer.ts
deleted file mode 100644
index 659cafb..0000000
--- a/ui/src/common/viewer.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-// Copyright (C) 2023 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 {Disposable} from '../base/disposable';
-import {globals} from '../frontend/globals';
-import {runQueryInNewTab} from '../frontend/query_result_tab';
-import {TrackPredicate, Viewer} from '../public';
-
-import {Actions} from './actions';
-
-class SidebarImpl {
- hide() {
- globals.dispatch(Actions.setSidebar({
- visible: false,
- }));
- }
-
- show() {
- globals.dispatch(Actions.setSidebar({
- visible: true,
- }));
- }
-
- isVisible() {
- return globals.state.sidebarVisible;
- }
-};
-
-class TracksImpl {
- pin(predicate: TrackPredicate) {
- const tracks = Object.values(globals.state.tracks);
- for (const track of tracks) {
- const tags = {
- name: track.name,
- };
- if (predicate(tags) && !this.isPinned(track.key)) {
- globals.dispatch(Actions.toggleTrackPinned({
- trackKey: track.key,
- }));
- }
- }
- }
-
- unpin(predicate: TrackPredicate) {
- const tracks = Object.values(globals.state.tracks);
- for (const track of tracks) {
- const tags = {
- name: track.name,
- };
- if (predicate(tags) && this.isPinned(track.key)) {
- globals.dispatch(Actions.toggleTrackPinned({
- trackKey: track.key,
- }));
- }
- }
- }
-
- private isPinned(trackId: string): boolean {
- return globals.state.pinnedTracks.includes(trackId);
- }
-};
-
-export class ViewerImpl implements Viewer {
- sidebar = new SidebarImpl();
- tracks = new TracksImpl();
-
- tabs = {
- openQuery: runQueryInNewTab,
- };
-
- commands = {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- run: (id: string, ...args: any[]) => {
- globals.commandManager.runCommand(id, ...args);
- },
- };
-
- constructor() {}
-
- getProxy(pluginId: string): ViewerProxy {
- return new ViewerProxy(this, pluginId);
- }
-}
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type AnyFunction = (...args: any[]) => any;
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type AnyProcedure = (...args: any[]) => void;
-
-function wrap<F extends AnyFunction>(
- allow: () => boolean, f: F, deadResult: ReturnType<F>) {
- return (...args: Parameters<F>) => {
- if (allow()) {
- return f(...args);
- } else {
- return deadResult;
- }
- };
-}
-
-function wrapVoid<F extends AnyProcedure>(allow: () => boolean, f: F) {
- return (...args: Parameters<F>) => {
- if (allow()) {
- f(...args);
- }
- };
-}
-
-export class ViewerProxy implements Viewer, Disposable {
- readonly parent: ViewerImpl;
- readonly pluginId: string;
- private alive: boolean;
-
- // ViewerImpl:
- sidebar: Viewer['sidebar'];
- tracks: Viewer['tracks'];
- tabs: Viewer['tabs'];
- commands: Viewer['commands'];
-
- // ViewerProxy:
- constructor(parent: ViewerImpl, pluginId: string) {
- this.parent = parent;
- this.pluginId = pluginId;
- this.alive = true;
- const allow = () => this.alive;
-
- const p = parent;
- this.sidebar = {
- hide: wrapVoid(allow, p.sidebar.hide.bind(p.sidebar)),
- show: wrapVoid(allow, p.sidebar.show.bind(p.sidebar)),
- isVisible: wrap(allow, p.sidebar.isVisible.bind(p.sidebar), false),
- };
-
- this.tracks = {
- pin: wrapVoid(allow, p.tracks.pin.bind(p.tracks)),
- unpin: wrapVoid(allow, p.tracks.unpin.bind(p.tracks)),
- };
-
- this.tabs = {
- openQuery: wrapVoid(allow, p.tabs.openQuery.bind(p.tabs)),
- };
-
- this.commands = {
- run: wrapVoid(allow, p.commands.run.bind(p.commands)),
- };
- }
-
- dispose(): void {
- this.alive = false;
- }
-}
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index c44ce90..5fb6b7b 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -62,7 +62,6 @@
publishRealtimeOffset,
publishThreads,
} from '../frontend/publish';
-import {runQueryInNewTab} from '../frontend/query_result_tab';
import {Router} from '../frontend/router';
import {Engine} from '../trace_processor/engine';
import {
@@ -616,7 +615,7 @@
pendingDeeplink.visStart, pendingDeeplink.visEnd);
}
if (pendingDeeplink.query !== undefined) {
- runQueryInNewTab(pendingDeeplink.query, 'Deeplink Query');
+ globals.openQuery(pendingDeeplink.query, 'Deeplink Query');
}
}
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 04d8012..a62cbf1 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -42,7 +42,6 @@
import {toggleHelp} from './help_modal';
import {fullscreenModalContainer} from './modal';
import {Omnibox, OmniboxOption} from './omnibox';
-import {runQueryInNewTab} from './query_result_tab';
import {verticalScrollToTrack} from './scroll_helper';
import {executeSearch} from './search_handler';
import {Sidebar} from './sidebar';
@@ -532,7 +531,7 @@
raf.scheduleFullRedraw();
},
onSubmit: (value, alt) => {
- runQueryInNewTab(
+ globals.openQuery(
undoCommonChatAppReplacements(value),
alt ? 'Pinned query' : 'Omnibox query',
alt ? undefined : 'omnibox_query');
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index d7f6073..91eb840 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -36,7 +36,6 @@
NewBottomTabArgs,
} from './bottom_tab';
import {FlowPoint, globals} from './globals';
-import {runQueryInNewTab} from './query_result_tab';
import {renderArguments} from './slice_args';
import {renderDetails} from './slice_details';
import {getSlice, SliceDetails, SliceRef} from './sql/slice';
@@ -92,7 +91,7 @@
{
name: 'Average duration of slice name',
shouldDisplay: (slice: SliceDetails) => hasName(slice),
- run: (slice: SliceDetails) => runQueryInNewTab(
+ run: (slice: SliceDetails) => globals.openQuery(
`SELECT AVG(dur) / 1e9 FROM slice WHERE name = '${slice.name!}'`,
`${slice.name} average dur`,
),
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index fa0d2ad..9a45e50 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -230,6 +230,8 @@
clearSearch?: boolean;
}
+type OpenQueryHandler = (query: string, title: string, tag?: string) => void;
+
/**
* Global accessors for state/dispatch in the frontend.
*/
@@ -275,6 +277,7 @@
private _cmdManager?: CommandManager = undefined;
private _realtimeOffset = Time.ZERO;
private _utcOffset = Time.ZERO;
+ private _openQueryHandler?: OpenQueryHandler;
// TODO(hjd): Remove once we no longer need to update UUID on redraw.
private _publishRedraw?: () => void = undefined;
@@ -839,6 +842,22 @@
return {start, end};
}
+
+ // The implementation of the query results tab is not part of the core so we
+ // decouple globals from the implementation using this registration interface.
+ // Once we move the implementation to a plugin, this decoupling will be
+ // simpler as we just need to call a command with a well-known ID, and a
+ // plugin will provide the implementation.
+ registerOpenQueryHandler(cb: OpenQueryHandler) {
+ this._openQueryHandler = cb;
+ }
+
+ // Runs a query and displays results in a new tab.
+ // Queries will override previously opened queries with the same tag.
+ // If the tag is omitted, the results will always open in a new tab.
+ openQuery(query: string, title: string, tag?: string) {
+ assertExists(this._openQueryHandler)(query, title, tag);
+ }
}
export const globals = new Globals();
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 0024ce8..3e24ef5 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -28,7 +28,6 @@
import {flattenArgs, traceEvent} from '../common/metatracing';
import {pluginManager, pluginRegistry} from '../common/plugins';
import {State} from '../common/state';
-import {ViewerImpl} from '../common/viewer';
import {initWasm} from '../common/wasm_engine_proxy';
import {initController, runControllers} from '../controller';
import {
@@ -292,10 +291,8 @@
document.body.classList.add('testing');
}
- // Initialize all plugins:
- const viewer = new ViewerImpl();
for (const plugin of pluginRegistry.values()) {
- pluginManager.activatePlugin(plugin.pluginId, viewer);
+ pluginManager.activatePlugin(plugin.pluginId);
}
cmdManager.registerCommandSource(pluginManager);
diff --git a/ui/src/frontend/query_result_tab.ts b/ui/src/frontend/query_result_tab.ts
index c6b40b6..f0efe26 100644
--- a/ui/src/frontend/query_result_tab.ts
+++ b/ui/src/frontend/query_result_tab.ts
@@ -34,6 +34,7 @@
closeTab,
NewBottomTabArgs,
} from './bottom_tab';
+import {globals} from './globals';
import {QueryTable} from './query_table';
export function runQueryInNewTab(query: string, title: string, tag?: string) {
@@ -47,6 +48,8 @@
});
}
+globals.registerOpenQueryHandler(runQueryInNewTab);
+
interface QueryResultTabConfig {
readonly query: string;
readonly title: string;
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
index 8947364..44e52af 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
@@ -15,15 +15,18 @@
import {
Plugin,
PluginContext,
+ PluginContextTrace,
PluginDescriptor,
} from '../../public';
class AndroidCujs implements Plugin {
- onActivate(ctx: PluginContext): void {
- ctx.addCommand({
+ onActivate(_ctx: PluginContext): void {}
+
+ async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+ ctx.registerCommand({
id: 'dev.perfetto.AndroidCujs#ListJankCUJs',
name: 'Run query: Android Jank CUJs',
- callback: () => ctx.viewer.tabs.openQuery(
+ callback: () => ctx.tabs.openQuery(
`
SELECT RUN_METRIC('android/android_jank_cuj.sql');
SELECT RUN_METRIC('android/jank/internal/counters.sql');
@@ -89,10 +92,10 @@
'Android Jank CUJs'),
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.AndroidCujs#ListLatencyCUJs',
name: 'Run query: Android Latency CUJs',
- callback: () => ctx.viewer.tabs.openQuery(
+ callback: () => ctx.tabs.openQuery(
`
SELECT
CASE
diff --git a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
index e1f0a57..a762a5e 100644
--- a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
@@ -44,7 +44,7 @@
}
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.AndroidNetwork#batteryEvents',
name: 'Run query: Pin battery events',
callback: async (track) => {
@@ -64,7 +64,7 @@
},
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.AndroidNetwork#activityTrack',
name: 'Run query: Visualize Network Activity',
callback: async (groupby, filter, trackName) => {
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
index feb7080..7658fb6 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
@@ -15,44 +15,45 @@
import {
Plugin,
PluginContext,
+ PluginContextTrace,
PluginDescriptor,
} from '../../public';
class AndroidPerf implements Plugin {
- onActivate(ctx: PluginContext): void {
- const {viewer} = ctx;
+ onActivate(_ctx: PluginContext): void {}
- ctx.addCommand({
+ async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+ ctx.registerCommand({
id: 'dev.perfetto.AndroidPerf#BinderSystemServerIncoming',
name: 'Run query: system_server incoming binder graph',
- callback: () => viewer.tabs.openQuery(
+ callback: () => ctx.tabs.openQuery(
`INCLUDE PERFETTO MODULE android.binder;
SELECT * FROM android_binder_incoming_graph((SELECT upid FROM process WHERE name = 'system_server'))`,
'system_server incoming binder graph'),
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.AndroidPerf#BinderSystemServerOutgoing',
name: 'Run query: system_server outgoing binder graph',
- callback: () => viewer.tabs.openQuery(
+ callback: () => ctx.tabs.openQuery(
`INCLUDE PERFETTO MODULE android.binder;
SELECT * FROM android_binder_outgoing_graph((SELECT upid FROM process WHERE name = 'system_server'))`,
'system_server outgoing binder graph'),
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.AndroidPerf#MonitorContentionSystemServer',
name: 'Run query: system_server monitor_contention graph',
- callback: () => viewer.tabs.openQuery(
+ callback: () => ctx.tabs.openQuery(
`INCLUDE PERFETTO MODULE android.monitor_contention;
SELECT * FROM android_monitor_contention_graph((SELECT upid FROM process WHERE name = 'system_server'))`,
'system_server monitor_contention graph'),
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.AndroidPerf#BinderAll',
name: 'Run query: all process binder graph',
- callback: () => viewer.tabs.openQuery(
+ callback: () => ctx.tabs.openQuery(
`INCLUDE PERFETTO MODULE android.binder;
SELECT * FROM android_binder_graph(-1000, 1000, -1000, 1000)`,
'all process binder graph'),
diff --git a/ui/src/plugins/dev.perfetto.CoreCommands/index.ts b/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
index 5b94d08..7024520 100644
--- a/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
+++ b/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
@@ -15,6 +15,7 @@
import {
Plugin,
PluginContext,
+ PluginContextTrace,
PluginDescriptor,
} from '../../public';
@@ -85,88 +86,88 @@
limit 100;`;
const coreCommands: Plugin = {
- onActivate: function(ctx: PluginContext): void {
- const {viewer} = ctx;
+ onActivate(_ctx: PluginContext) {},
- ctx.addCommand({
+ async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+ ctx.registerCommand({
id: 'dev.perfetto.CoreCommands#ToggleLeftSidebar',
name: 'Toggle left sidebar',
callback: () => {
- if (viewer.sidebar.isVisible()) {
- viewer.sidebar.hide();
+ if (ctx.sidebar.isVisible()) {
+ ctx.sidebar.hide();
} else {
- viewer.sidebar.show();
+ ctx.sidebar.show();
}
},
defaultHotkey: '!Mod+B',
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.CoreCommands#RunQueryAllProcesses',
name: 'Run query: all processes',
callback: () => {
- viewer.tabs.openQuery(ALL_PROCESSES_QUERY, 'All Processes');
+ ctx.tabs.openQuery(ALL_PROCESSES_QUERY, 'All Processes');
},
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.CoreCommands#RunQueryCpuTimeByProcess',
name: 'Run query: CPU time by process',
callback: () => {
- viewer.tabs.openQuery(CPU_TIME_FOR_PROCESSES, 'CPU time by process');
+ ctx.tabs.openQuery(CPU_TIME_FOR_PROCESSES, 'CPU time by process');
},
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.CoreCommands#RunQueryCyclesByStateByCpu',
name: 'Run query: cycles by p-state by CPU',
callback: () => {
- viewer.tabs.openQuery(
+ ctx.tabs.openQuery(
CYCLES_PER_P_STATE_PER_CPU, 'Cycles by p-state by CPU');
},
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.CoreCommands#RunQueryCyclesByCpuByProcess',
name: 'Run query: CPU Time by CPU by process',
callback: () => {
- viewer.tabs.openQuery(
+ ctx.tabs.openQuery(
CPU_TIME_BY_CPU_BY_PROCESS, 'CPU Time by CPU by process');
},
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.CoreCommands#RunQueryHeapGraphBytesPerType',
name: 'Run query: heap graph bytes per type',
callback: () => {
- viewer.tabs.openQuery(
+ ctx.tabs.openQuery(
HEAP_GRAPH_BYTES_PER_TYPE, 'Heap graph bytes per type');
},
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.CoreCommands#DebugSqlPerformance',
name: 'Debug SQL performance',
callback: () => {
- viewer.tabs.openQuery(SQL_STATS, 'Recent SQL queries');
+ ctx.tabs.openQuery(SQL_STATS, 'Recent SQL queries');
},
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.CoreCommands#PinFtraceTracks',
name: 'Pin ftrace tracks',
callback: () => {
- viewer.tracks.pin((tags) => {
+ ctx.timeline.pinTracksByPredicate((tags) => {
return !!tags.name?.startsWith('Ftrace Events Cpu ');
});
},
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.CoreCommands#UnpinAllTracks',
name: 'Unpin all tracks',
callback: () => {
- viewer.tracks.unpin((_) => {
+ ctx.timeline.unpinTracksByPredicate((_) => {
return true;
});
},
diff --git a/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts b/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
index 4bb6e12..d09462c 100644
--- a/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
+++ b/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
@@ -21,7 +21,7 @@
// This is just an example plugin, used to prove that the plugin system works.
class ExampleSimpleCommand implements Plugin {
onActivate(ctx: PluginContext): void {
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.ExampleSimpleCommand#LogHelloWorld',
name: 'Log "Hello, world!"',
callback: () => console.log('Hello, world!'),
diff --git a/ui/src/plugins/dev.perfetto.ExampleState/index.ts b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
index 9e4e31c..4470e6c 100644
--- a/ui/src/plugins/dev.perfetto.ExampleState/index.ts
+++ b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
@@ -40,15 +40,14 @@
}
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
- const {viewer} = ctx;
const store = ctx.mountStore((init: unknown) => this.migrate(init));
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.ExampleState#ShowCounter',
name: 'Show ExampleState counter',
callback: () => {
const counter = store.state.counter;
- viewer.tabs.openQuery(
+ ctx.tabs.openQuery(
`SELECT ${counter} as counter;`, `Show counter ${counter}`);
store.edit((draft) => ++draft.counter);
},
diff --git a/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts b/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
index 07872ed..9c05772 100644
--- a/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
@@ -15,16 +15,19 @@
import {
Plugin,
PluginContext,
+ PluginContextTrace,
PluginDescriptor,
} from '../../public';
class LargeScreensPerf implements Plugin {
- onActivate(ctx: PluginContext): void {
- ctx.addCommand({
+ onActivate(_ctx: PluginContext): void {}
+
+ async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+ ctx.registerCommand({
id: 'dev.perfetto.LargeScreensPerf#PinUnfoldLatencyTracks',
name: 'Pin: Unfold latency tracks',
callback: () => {
- ctx.viewer.tracks.pin((tags) => {
+ ctx.timeline.pinTracksByPredicate((tags) => {
return !!tags.name?.includes('UNFOLD') ||
tags.name?.includes('Screen on blocked') ||
tags.name?.startsWith('waitForAllWindowsDrawn') ||
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index cc2fb77..e0d7c9d 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -32,35 +32,6 @@
STR_NULL,
} from '../trace_processor/query_result';
-
-// An imperative API for plugins to change the UI.
-export interface Viewer {
- // Control of the sidebar.
- sidebar: {
- // Show the sidebar.
- show(): void;
- // Hide the sidebar.
- hide(): void;
- // Returns true if the sidebar is visble.
- isVisible(): boolean;
- }
-
- // Tracks
- tracks: {
- pin(predicate: TrackPredicate): void;
- unpin(predicate: TrackPredicate): void;
- }
-
- // Control over the bottom details pane.
- tabs: {
- // Creates a new tab running the provided query.
- openQuery(query: string, title: string): void;
- }
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- commands: {run(name: string, ...args: any[]): void;}
-}
-
export interface Command {
// A unique id for this command.
id: string;
@@ -68,7 +39,7 @@
name: string;
// Callback is called when the command is invoked.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- callback: (...args: any[]) => void;
+ callback: (...args: any[]) => any;
// Default hotkey for this command.
// Note: this is just the default and may be changed by the user.
// Examples:
@@ -118,11 +89,27 @@
// The unique ID for this plugin.
readonly pluginId: string;
- // The viewer API, used to interface with Perfetto.
- readonly viewer: Viewer;
+ // Register command against this plugin context.
+ registerCommand(command: Command): void;
- // Add a command.
- addCommand(command: Command): void;
+ // Retrieve a list of all commands.
+ commands: Command[];
+
+ // Run a command, optionally passing some args.
+ // 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;
+ };
}
export type Migrate<State> = (init: unknown) => State;
@@ -166,7 +153,7 @@
// A unique identifier for this track.
uri: string;
- // A factory function returning a track object.
+ // A factory function returning the track object.
track: (ctx: TrackContext) => Track;
// The track "kind", used by various subsystems e.g. aggregation controllers.
@@ -224,27 +211,96 @@
ORDINARY_TRACK,
}
+export interface SliceTrackColNames {
+ ts: string;
+ name: string;
+ dur: string;
+}
+
+export interface DebugSliceTrackArgs {
+ // Title of the track. If omitted a placeholder name will be chosen instead.
+ trackName?: string;
+
+ // Mapping definitions of the 'ts', 'dur', and 'name' columns.
+ // By default, columns called ts, dur and name will be used.
+ // If dur is assigned the value '0', all slices shall be instant events.
+ columnMapping?: Partial<SliceTrackColNames>;
+
+ // Any extra columns to be used as args.
+ args?: string[];
+
+ // Optional renaming of columns.
+ columns?: string[];
+}
+
+export interface CounterTrackColNames {
+ ts: string;
+ value: string;
+}
+
+export interface DebugCounterTrackArgs {
+ // Title of the track. If omitted a placeholder name will be chosen instead.
+ trackName?: string;
+
+ // Mapping definitions of the ts and value columns.
+ columnMapping?: Partial<CounterTrackColNames>;
+}
+
// Similar to PluginContext but with additional methods to operate on the
// currently loaded trace. Passed to trace-relevant hooks on a plugin instead of
// PluginContext.
export interface PluginContextTrace extends PluginContext {
readonly engine: EngineProxy;
+ // Control over the main timeline.
+ timeline: {
+ // Add a new track to the scrolling track section, returning the newly
+ // created track key.
+ addTrack(uri: string, displayName: string, params?: unknown): string;
+
+ // Remove a single track from the timeline.
+ removeTrack(key: string): void;
+
+ // Pin a single track.
+ pinTrack(key: string): void;
+
+ // Unpin a single track.
+ unpinTrack(key: string): void;
+
+ // Pin all tracks that match a predicate.
+ pinTracksByPredicate(predicate: TrackPredicate): void;
+
+ // Unpin all tracks that match a predicate.
+ unpinTracksByPredicate(predicate: TrackPredicate): void;
+
+ // Remove all tracks that match a predicate.
+ removeTracksByPredicate(predicate: TrackPredicate): void;
+
+ // Retrieve a list of tracks on the timeline.
+ tracks: TrackRef[];
+ }
+
+ // Control over the bottom details pane.
+ tabs: {
+ // Creates a new tab running the provided query.
+ openQuery(query: string, title: string): void;
+ }
+
// Register a new track against a unique key known as a URI.
// Once a track is registered it can be referenced multiple times on the
- // timeline.
+ // timeline with different params to allow customising each instance.
registerTrack(trackDesc: TrackDescriptor): void;
- // Add a new entry to the pool of default tracks. Default tracks are a list of
- // track references that describe the list of tracks that should be added to
- // the main timeline on startup.
- // Default tracks are only used when a trace is first loaded, not when loading
- // from a permalink, where the existing list of tracks from the shared state
- // is used instead.
+ // Add a new entry to the pool of default tracks. Default tracks are a list
+ // of track references that describe the list of tracks that should be added
+ // to the main timeline on startup.
+ // Default tracks are only used when a trace is first loaded, not when
+ // loading from a permalink, where the existing list of tracks from the
+ // shared state is used instead.
addDefaultTrack(track: TrackRef): void;
// Simultaneously register a track and add it as a default track in one go.
- // This is simply a helper which calls registerTrack() then addDefaultTrack()
+ // This is simply a helper which calls registerTrack() and addDefaultTrack()
// with the same URI.
registerStaticTrack(track: TrackDescriptor&TrackRef): void;
diff --git a/ui/src/tracks/chrome_critical_user_interactions/index.ts b/ui/src/tracks/chrome_critical_user_interactions/index.ts
index 3d1b498..b77fc31 100644
--- a/ui/src/tracks/chrome_critical_user_interactions/index.ts
+++ b/ui/src/tracks/chrome_critical_user_interactions/index.ts
@@ -96,7 +96,7 @@
}
onActivate(ctx: PluginContext): void {
- ctx.addCommand({
+ ctx.registerCommand({
id: 'perfetto.CriticalUserInteraction.AddInteractionTrack',
name: 'Add Chrome Interactions track',
callback: () => addCriticalUserInteractionTrack(),
diff --git a/ui/src/tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts b/ui/src/tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
index 2461c66..83a5a14 100644
--- a/ui/src/tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
@@ -13,11 +13,11 @@
// limitations under the License.
import {InThreadTrackSortKey} from '../../common/state';
+import {globals} from '../../frontend/globals';
import {
NamedSliceTrack,
NamedSliceTrackTypes,
} from '../../frontend/named_slice_track';
-import {runQueryInNewTab} from '../../frontend/query_result_tab';
import {NewTrackArgs, TrackBase} from '../../frontend/track';
import {Engine} from '../../trace_processor/engine';
import {NUM} from '../../trace_processor/query_result';
@@ -109,7 +109,8 @@
from chrome_tasks_delaying_input_processing s1
join slice s2 on s1.slice_id=s2.id
`;
- runQueryInNewTab(query, 'Scroll Jank: long tasks');
+ // TODO(stevegolton): This will soon be replaced by ctx.openQuery.
+ globals.openQuery(query, 'Scroll Jank: long tasks');
return result;
}
diff --git a/ui/src/tracks/sched/index.ts b/ui/src/tracks/sched/index.ts
index f9bba55..af411ed 100644
--- a/ui/src/tracks/sched/index.ts
+++ b/ui/src/tracks/sched/index.ts
@@ -46,18 +46,18 @@
}
onActivate(ctx: PluginContext): void {
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.Sched.AddRunnableThreadCountTrackCommand',
name: 'Add runnable thread count track',
callback: () => addRunnableThreadCountTrack(),
});
- ctx.addCommand({
+ ctx.registerCommand({
id: 'dev.perfetto.Sched.AddActiveCPUCountTrackCommand',
name: 'Add active CPU count track',
callback: () => addActiveCPUCountTrack(),
});
for (const cpuType of ['big', 'little', 'mid']) {
- ctx.addCommand({
+ ctx.registerCommand({
id: `dev.perfetto.Sched.AddActiveCPUCountTrackCommand.${cpuType}`,
name: `Add active ${cpuType} CPU count track`,
callback: () => addActiveCPUCountTrack(cpuType),