Merge "Increase granularity of Lock contention blocking calls" into main
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 9535b08..afadad0 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -20,7 +20,7 @@
import {globals} from '../frontend/globals';
import {
Command,
- CurrentSelectionSection,
+ DetailsPanel,
EngineProxy,
MetricVisualisation,
Migrate,
@@ -41,6 +41,7 @@
import {Actions} from './actions';
import {Registry} from './registry';
import {SCROLLING_TRACK_GROUP} from './state';
+import {addQueryResultsTab} from '../frontend/query_result_tab';
// 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
@@ -164,13 +165,13 @@
this.trash.addCallback(() => globals.tabManager.unregisterTab(desc.uri));
}
- registerCurrentSelectionSection(section: CurrentSelectionSection): void {
+ registerDetailsPanel(section: DetailsPanel): void {
if (!this.alive) return;
const tabMan = globals.tabManager;
- tabMan.registerCurrentSelectionSection(section);
+ tabMan.registerDetailsPanel(section);
this.trash.addCallback(
- () => tabMan.unregisterCurrentSelectionSection(section));
+ () => tabMan.unregisterDetailsPanel(section));
}
get sidebar() {
@@ -179,7 +180,7 @@
readonly tabs = {
openQuery: (query: string, title: string) => {
- globals.openQuery(query, title);
+ addQueryResultsTab({query, title});
},
showTab(uri: string):
diff --git a/ui/src/common/tab_registry.ts b/ui/src/common/tab_registry.ts
index d564c9b..c9faca7 100644
--- a/ui/src/common/tab_registry.ts
+++ b/ui/src/common/tab_registry.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import {Disposable} from '../base/disposable';
-import {CurrentSelectionSection, TabDescriptor} from '../public';
+import {DetailsPanel, TabDescriptor} from '../public';
export interface ResolvedTab {
uri: string;
@@ -26,7 +26,7 @@
*/
export class TabManager implements Disposable {
private _registry = new Map<string, TabDescriptor>();
- private _currentSelectionSectionReg = new Set<CurrentSelectionSection>();
+ private _detailsPanelsRegistry = new Set<DetailsPanel>();
private _currentTabs = new Map<string, TabDescriptor>();
dispose(): void {
@@ -45,12 +45,12 @@
this._registry.delete(uri);
}
- registerCurrentSelectionSection(section: CurrentSelectionSection): void {
- this._currentSelectionSectionReg.add(section);
+ registerDetailsPanel(section: DetailsPanel): void {
+ this._detailsPanelsRegistry.add(section);
}
- unregisterCurrentSelectionSection(section: CurrentSelectionSection): void {
- this._currentSelectionSectionReg.delete(section);
+ unregisterDetailsPanel(section: DetailsPanel): void {
+ this._detailsPanelsRegistry.delete(section);
}
resolveTab(uri: string): TabDescriptor|undefined {
@@ -61,8 +61,8 @@
return Array.from(this._registry.values());
}
- get currentSelectionSections(): CurrentSelectionSection[] {
- return Array.from(this._currentSelectionSectionReg);
+ get detailsPanels(): DetailsPanel[] {
+ return Array.from(this._detailsPanelsRegistry);
}
/**
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index f752427..b5c6994 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -59,6 +59,7 @@
publishRealtimeOffset,
publishThreads,
} from '../frontend/publish';
+import {addQueryResultsTab} from '../frontend/query_result_tab';
import {Router} from '../frontend/router';
import {Engine} from '../trace_processor/engine';
import {HttpRpcEngine} from '../trace_processor/http_rpc_engine';
@@ -629,7 +630,10 @@
pendingDeeplink.visStart, pendingDeeplink.visEnd);
}
if (pendingDeeplink.query !== undefined) {
- globals.openQuery(pendingDeeplink.query, 'Deeplink Query');
+ addQueryResultsTab({
+ query: pendingDeeplink.query,
+ title: 'Deeplink Query',
+ });
}
}
diff --git a/ui/src/core/feature_flags.ts b/ui/src/core/feature_flags.ts
index c6c98bd..f00d535 100644
--- a/ui/src/core/feature_flags.ts
+++ b/ui/src/core/feature_flags.ts
@@ -244,3 +244,10 @@
description: 'Record using V2 interface',
defaultValue: false,
});
+
+export const TABS_V2_FLAG = featureFlags.register({
+ id: 'tabsv2',
+ name: 'Tabs V2',
+ description: 'Use Tabs V2',
+ defaultValue: false,
+});
diff --git a/ui/src/frontend/aggregation_panel.ts b/ui/src/frontend/aggregation_panel.ts
index c7f3eb8..5232759 100644
--- a/ui/src/frontend/aggregation_panel.ts
+++ b/ui/src/frontend/aggregation_panel.ts
@@ -19,21 +19,30 @@
AggregateData,
Column,
ThreadStateExtra,
+ isEmptyData,
} from '../common/aggregation_data';
import {colorForState} from '../common/colorizer';
import {translateState} from '../common/thread_state';
import {globals} from './globals';
import {DurationWidget} from './widgets/duration';
+import {EmptyState} from '../widgets/empty_state';
export interface AggregationPanelAttrs {
- data: AggregateData;
+ data?: AggregateData;
kind: string;
}
export class AggregationPanel implements
m.ClassComponent<AggregationPanelAttrs> {
view({attrs}: m.CVnode<AggregationPanelAttrs>) {
+ if (!attrs.data || isEmptyData(attrs.data)) {
+ return m(EmptyState, {
+ className: 'pf-noselection',
+ header: 'Nothing selected',
+ detail: 'Aggregation data will appear here',
+ });
+ }
return m(
'.details-panel',
m('.details-panel-heading.aggregation',
diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts
new file mode 100644
index 0000000..89b2044
--- /dev/null
+++ b/ui/src/frontend/aggregation_tab.ts
@@ -0,0 +1,68 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use size 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 {AggregationPanel} from './aggregation_panel';
+import {globals} from './globals';
+
+export class AggregationsTabs implements Disposable {
+ private tabs = [
+ {
+ type: 'cpu_aggregation',
+ title: 'CPU by thread',
+ },
+ {
+ type: 'thread_state_aggregation',
+ title: 'Thread States',
+ },
+ {
+ type: 'cpu_by_process_aggregation',
+ title: 'CPU by process',
+ },
+ {
+ type: 'slice_aggregation',
+ title: 'Slices',
+ },
+ {
+ type: 'counter_aggregation',
+ title: 'Counters',
+ },
+ {
+ type: 'frame_aggregation',
+ title: 'Frames',
+ },
+ ];
+
+ constructor() {
+ for (const {type, title} of this.tabs) {
+ globals.tabManager.registerTab({
+ uri: `aggregationTab#${type}`,
+ isEphemeral: false,
+ content: {
+ getTitle: () => `Aggregation: ${title}`,
+ render: () => {
+ const data = globals.aggregateDataStore.get(type);
+ return m(AggregationPanel, {kind: type, data});
+ },
+ },
+ });
+ }
+ }
+
+ dispose(): void {
+ for (const {type} of this.tabs) {
+ globals.tabManager.unregisterTab(`aggregationTab#${type}`);
+ }
+ }
+}
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 8eed31d..9d634fe 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -43,23 +43,24 @@
import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
import {maybeRenderFullscreenModalDialog} from '../widgets/modal';
-import {addTab} from './bottom_tab';
import {onClickCopy} from './clipboard';
import {CookieConsent} from './cookie_consent';
import {globals} from './globals';
import {toggleHelp} from './help_modal';
+import {Notes} from './notes';
import {Omnibox, OmniboxOption} from './omnibox';
-import {runQueryInNewTab} from './query_result_tab';
+import {addQueryResultsTab} from './query_result_tab';
import {verticalScrollToTrack} from './scroll_helper';
import {executeSearch} from './search_handler';
import {Sidebar} from './sidebar';
-import {SqlTableTab} from './sql_table/tab';
-import {SqlTables} from './sql_table/well_known_tables';
import {Utid} from './sql_types';
import {getThreadInfo} from './thread_and_process_info';
import {Topbar} from './topbar';
import {shareTrace} from './trace_attrs';
import {addDebugSliceTrack} from './debug_tracks';
+import {AggregationsTabs} from './aggregation_tab';
+import {addSqlTableTab} from './sql_table/tab';
+import {SqlTables} from './sql_table/well_known_tables';
function renderPermalink(): m.Children {
const permalink = globals.state.permalink;
@@ -149,6 +150,8 @@
constructor() {
const unreg = globals.commandManager.registerCommandSource(this);
this.trash.add(unreg);
+ this.trash.add(new Notes());
+ this.trash.add(new AggregationsTabs());
}
private getEngine(): EngineProxy|undefined {
@@ -395,8 +398,8 @@
const engine = this.getEngine();
if (engine !== undefined && trackUtid != 0) {
- runQueryInNewTab(
- `SELECT IMPORT('experimental.thread_executing_span');
+ addQueryResultsTab({
+ query: `SELECT IMPORT('experimental.thread_executing_span');
SELECT *
FROM
experimental_thread_executing_span_critical_path_graph(
@@ -404,8 +407,7 @@
${trackUtid},
${window.start},
${window.end} - ${window.start}) cr`,
- 'Critical path',
- 'omnibox_query');
+ title: 'Critical path'});
}
},
},
@@ -414,12 +416,9 @@
name: 'Show slice table',
callback:
() => {
- addTab({
- kind: SqlTableTab.kind,
- config: {
- table: SqlTables.slice,
- displayName: 'slice',
- },
+ addSqlTableTab({
+ table: SqlTables.slice,
+ displayName: 'slice',
});
},
},
@@ -722,11 +721,13 @@
this.queryText = value;
raf.scheduleFullRedraw();
},
- onSubmit: (value, alt) => {
- globals.openQuery(
- undoCommonChatAppReplacements(value),
- alt ? 'Pinned query' : 'Omnibox query',
- alt ? undefined : 'omnibox_query');
+ onSubmit: (query, alt) => {
+ const config = {
+ query: undoCommonChatAppReplacements(query),
+ title: alt ? 'Pinned query' : 'Omnibox query',
+ };
+ const tag = alt? undefined : 'omnibox_query';
+ addQueryResultsTab(config, tag);
},
onClose: () => {
this.queryText = '';
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 6f5502b..920c8c8 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -44,6 +44,7 @@
import {asSliceSqlId} from './sql_types';
import {DurationWidget} from './widgets/duration';
import {addDebugSliceTrack} from './debug_tracks';
+import {addQueryResultsTab} from './query_result_tab';
interface ContextMenuItem {
name: string;
@@ -91,10 +92,10 @@
{
name: 'Average duration of slice name',
shouldDisplay: (slice: SliceDetails) => hasName(slice),
- run: (slice: SliceDetails) => globals.openQuery(
- `SELECT AVG(dur) / 1e9 FROM slice WHERE name = '${slice.name!}'`,
- `${slice.name} average dur`,
- ),
+ run: (slice: SliceDetails) => addQueryResultsTab({
+ query: `SELECT AVG(dur) / 1e9 FROM slice WHERE name = '${slice.name!}'`,
+ title: `${slice.name} average dur`,
+ }),
},
{
name: 'Binder txn names + monitor contention on thread',
diff --git a/ui/src/frontend/drag_handle.ts b/ui/src/frontend/drag_handle.ts
index 425899c..5176039 100644
--- a/ui/src/frontend/drag_handle.ts
+++ b/ui/src/frontend/drag_handle.ts
@@ -47,6 +47,9 @@
// Called when tab dropdown entry is clicked.
onClick: () => void;
+
+ // Whether this tab is checked or not
+ checked: boolean;
}
export interface DragHandleAttrs {
@@ -241,6 +244,7 @@
key: entry.key,
label: entry.title,
onclick: () => entry.onClick(),
+ icon: entry.checked ? 'check_box' : 'check_box_outline_blank',
});
}),
);
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 205beab..2f0cd7f 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -238,8 +238,6 @@
clearSearch?: boolean;
}
-type OpenQueryHandler = (query: string, title: string, tag?: string) => void;
-
/**
* Global accessors for state/dispatch in the frontend.
*/
@@ -286,7 +284,6 @@
private _realtimeOffset = Time.ZERO;
private _utcOffset = Time.ZERO;
private _traceTzOffset = Time.ZERO;
- private _openQueryHandler?: OpenQueryHandler;
private _tabManager = new TabManager();
private _trackManager = new TrackManager(this._store);
@@ -872,22 +869,6 @@
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);
- }
-
panToTimestamp(ts: time): void {
horizontalScrollToTs(ts);
}
diff --git a/ui/src/frontend/notes.ts b/ui/src/frontend/notes.ts
new file mode 100644
index 0000000..d4b86a9
--- /dev/null
+++ b/ui/src/frontend/notes.ts
@@ -0,0 +1,45 @@
+import {Disposable} from '../base/disposable';
+import {assertExists} from '../base/logging';
+import {uuidv4} from '../base/uuid';
+import {BottomTabToSCSAdapter} from '../public';
+
+import {globals} from './globals';
+import {NotesEditorTab} from './notes_panel';
+
+function getEngine() {
+ const engineId = assertExists(globals.getCurrentEngine()).id;
+ const engine = assertExists(globals.engines.get(engineId));
+ return engine;
+}
+
+/**
+ * Registers with the tab manager to show notes details panels when notes are
+ * selected.
+ *
+ * Notes are core functionality thus don't really belong in a plugin.
+ */
+export class Notes implements Disposable {
+ private csc = new BottomTabToSCSAdapter({
+ tabFactory: (selection) => {
+ if (selection.kind === 'NOTE') {
+ return new NotesEditorTab({
+ config: {
+ id: selection.id,
+ },
+ engine: getEngine().getProxy('Notes'),
+ uuid: uuidv4(),
+ });
+ } else {
+ return undefined;
+ }
+ },
+ });
+
+ constructor() {
+ globals.tabManager.registerDetailsPanel(this.csc);
+ }
+
+ dispose(): void {
+ globals.tabManager.unregisterDetailsPanel(this.csc);
+ }
+}
diff --git a/ui/src/frontend/pivot_table.ts b/ui/src/frontend/pivot_table.ts
index 483ead2..d241534 100644
--- a/ui/src/frontend/pivot_table.ts
+++ b/ui/src/frontend/pivot_table.ts
@@ -29,7 +29,6 @@
import {raf} from '../core/raf_scheduler';
import {ColumnType} from '../trace_processor/query_result';
-import {addTab} from './bottom_tab';
import {globals} from './globals';
import {
aggregationIndex,
@@ -47,7 +46,7 @@
} from './pivot_table_types';
import {PopupMenuButton, popupMenuIcon, PopupMenuItem} from './popup_menu';
import {ReorderableCell, ReorderableCellGroup} from './reorderable_cells';
-import {SqlTableTab} from './sql_table/tab';
+import {addSqlTableTab} from './sql_table/tab';
import {SqlTables} from './sql_table/well_known_tables';
import {AttributeModalHolder} from './tables/attribute_modal_holder';
import {DurationWidget} from './widgets/duration';
@@ -136,12 +135,9 @@
if (this.constrainToArea) {
queryFilters.push(...areaFilters(area));
}
- addTab({
- kind: SqlTableTab.kind,
- config: {
- table: SqlTables.slice,
- filters: queryFilters,
- },
+ addSqlTableTab({
+ table: SqlTables.slice,
+ filters: queryFilters,
});
},
},
diff --git a/ui/src/frontend/query_page.ts b/ui/src/frontend/query_page.ts
index 98a4881..4ff1715 100644
--- a/ui/src/frontend/query_page.ts
+++ b/ui/src/frontend/query_page.ts
@@ -24,11 +24,10 @@
import {Callout} from '../widgets/callout';
import {Editor} from '../widgets/editor';
-import {addTab} from './bottom_tab';
import {globals} from './globals';
import {createPage} from './pages';
import {QueryHistoryComponent, queryHistoryStorage} from './query_history';
-import {QueryResultTab} from './query_result_tab';
+import {addQueryResultsTab} from './query_result_tab';
import {QueryTable} from './query_table';
interface QueryPageState {
@@ -52,15 +51,14 @@
if (engine) {
runQuery(undoCommonChatAppReplacements(query), engine)
.then((resp: QueryResponse) => {
- addTab({
- kind: QueryResultTab.kind,
- tag: 'analyze_page_query',
- config: {
+ addQueryResultsTab(
+ {
query: query,
title: 'Standalone Query',
prefetchedResponse: resp,
},
- });
+ 'analyze_page_query',
+ );
// We might have started to execute another query. Ignore it in that
// case.
if (state.executedQuery !== query) {
diff --git a/ui/src/frontend/query_result_tab.ts b/ui/src/frontend/query_result_tab.ts
index 2f0c903..d42dd48 100644
--- a/ui/src/frontend/query_result_tab.ts
+++ b/ui/src/frontend/query_result_tab.ts
@@ -34,21 +34,12 @@
closeTab,
NewBottomTabArgs,
} from './bottom_tab';
-import {globals} from './globals';
import {QueryTable} from './query_table';
-
-export function runQueryInNewTab(query: string, title: string, tag?: string) {
- return addTab({
- kind: QueryResultTab.kind,
- tag,
- config: {
- query,
- title,
- },
- });
-}
-
-globals.registerOpenQueryHandler(runQueryInNewTab);
+import {TABS_V2_FLAG} from '../core/feature_flags';
+import {globals} from './globals';
+import {Actions} from '../common/actions';
+import {BottomTabToTabAdapter} from '../public/utils';
+import {EngineProxy} from '../public';
interface QueryResultTabConfig {
readonly query: string;
@@ -58,6 +49,42 @@
readonly prefetchedResponse?: QueryResponse;
}
+// External interface for adding a new query results tab
+// Automatically decided whether to add v1 or v2 tab
+export function addQueryResultsTab(
+ config: QueryResultTabConfig, tag?: string): void {
+ if (TABS_V2_FLAG.get()) {
+ const queryResultsTab = new QueryResultTab({
+ config,
+ engine: getEngine(),
+ uuid: uuidv4(),
+ });
+
+ const uri = 'queryResults#' + (tag ?? uuidv4());
+
+ globals.tabManager.registerTab({
+ uri,
+ content: new BottomTabToTabAdapter(queryResultsTab),
+ isEphemeral: true,
+ });
+
+ globals.dispatch(Actions.showTab({uri}));
+ } else {
+ return addTab({
+ kind: QueryResultTab.kind,
+ tag,
+ config,
+ });
+ }
+}
+
+// TODO(stevegolton): Find a way to make this more elegant.
+function getEngine(): EngineProxy {
+ const engConfig = globals.getCurrentEngine();
+ const engineId = assertExists(engConfig).id;
+ return assertExists(globals.engines.get(engineId)).getProxy('QueryResult');
+}
+
export class QueryResultTab extends BottomTab<QueryResultTabConfig> {
static readonly kind = 'dev.perfetto.QueryResultTab';
diff --git a/ui/src/frontend/slice_args.ts b/ui/src/frontend/slice_args.ts
index 9d4c122..341fbe1 100644
--- a/ui/src/frontend/slice_args.ts
+++ b/ui/src/frontend/slice_args.ts
@@ -32,10 +32,9 @@
import {MenuItem, PopupMenu2} from '../widgets/menu';
import {TreeNode} from '../widgets/tree';
-import {addTab} from './bottom_tab';
import {globals} from './globals';
import {Arg} from './sql/args';
-import {SqlTableTab} from './sql_table/tab';
+import {addSqlTableTab} from './sql_table/tab';
import {SqlTables} from './sql_table/well_known_tables';
// Renders slice arguments (key/value pairs) as a subtree.
@@ -96,17 +95,14 @@
label: 'Find slices with same arg value',
icon: 'search',
onclick: () => {
- addTab({
- kind: SqlTableTab.kind,
- config: {
- table: SqlTables.slice,
- filters: [{
- type: 'arg_filter',
- argSetIdColumn: 'arg_set_id',
- argName: fullKey,
- op: `= ${sqliteString(displayValue)}`,
- }],
- },
+ addSqlTableTab({
+ table: SqlTables.slice,
+ filters: [{
+ type: 'arg_filter',
+ argSetIdColumn: 'arg_set_id',
+ argName: fullKey,
+ op: `= ${sqliteString(displayValue)}`,
+ }],
});
},
}),
diff --git a/ui/src/frontend/slice_details.ts b/ui/src/frontend/slice_details.ts
index 282ecc8..c8c97b0 100644
--- a/ui/src/frontend/slice_details.ts
+++ b/ui/src/frontend/slice_details.ts
@@ -24,14 +24,13 @@
import {SqlRef} from '../widgets/sql_ref';
import {Tree, TreeNode} from '../widgets/tree';
-import {addTab} from './bottom_tab';
import {globals} from './globals';
import {SliceDetails} from './sql/slice';
import {
BreakdownByThreadState,
BreakdownByThreadStateTreeNode,
} from './sql/thread_state';
-import {SqlTableTab} from './sql_table/tab';
+import {addSqlTableTab} from './sql_table/tab';
import {SqlTables} from './sql_table/well_known_tables';
import {getProcessName, getThreadName} from './thread_and_process_info';
import {DurationWidget} from './widgets/duration';
@@ -65,13 +64,10 @@
m(MenuItem, {
label: 'Slices with the same name',
onclick: () => {
- addTab({
- kind: SqlTableTab.kind,
- config: {
- table: SqlTables.slice,
- displayName: 'slice',
- filters: [`name = ${sqliteString(slice.name)}`],
- },
+ addSqlTableTab({
+ table: SqlTables.slice,
+ displayName: 'slice',
+ filters: [`name = ${sqliteString(slice.name)}`],
});
},
}),
diff --git a/ui/src/frontend/sql_table/tab.ts b/ui/src/frontend/sql_table/tab.ts
index bd704cd..109f398 100644
--- a/ui/src/frontend/sql_table/tab.ts
+++ b/ui/src/frontend/sql_table/tab.ts
@@ -21,16 +21,55 @@
import {Button} from '../../widgets/button';
import {DetailsShell} from '../../widgets/details_shell';
import {Popup, PopupPosition} from '../../widgets/popup';
-import {BottomTab, bottomTabRegistry, NewBottomTabArgs} from '../bottom_tab';
+import {addTab, BottomTab, bottomTabRegistry, NewBottomTabArgs} from '../bottom_tab';
-import {SqlTableState} from './state';
+import {Filter, SqlTableState} from './state';
import {SqlTable} from './table';
import {SqlTableDescription, tableDisplayName} from './table_description';
+import {TABS_V2_FLAG} from '../../core/feature_flags';
+import {EngineProxy} from '../../public';
+import {globals} from '../globals';
+import {assertExists} from '../../base/logging';
+import {uuidv4} from '../../base/uuid';
+import {BottomTabToTabAdapter} from '../../public/utils';
+import {Actions} from '../../common/actions';
interface SqlTableTabConfig {
table: SqlTableDescription;
displayName?: string;
- filters?: string[];
+ filters?: Filter[];
+}
+
+export function addSqlTableTab(config: SqlTableTabConfig): void {
+ if (TABS_V2_FLAG.get()) {
+ const queryResultsTab = new SqlTableTab({
+ config,
+ engine: getEngine(),
+ uuid: uuidv4(),
+ });
+
+ const uri = 'sqlTable#' + uuidv4();
+
+ globals.tabManager.registerTab({
+ uri,
+ content: new BottomTabToTabAdapter(queryResultsTab),
+ isEphemeral: true,
+ });
+
+ globals.dispatch(Actions.showTab({uri}));
+ } else {
+ return addTab({
+ kind: SqlTableTab.kind,
+ config,
+ });
+ }
+}
+
+// TODO(stevegolton): Find a way to make this more elegant.
+function getEngine(): EngineProxy {
+ const engConfig = globals.getCurrentEngine();
+ const engineId = assertExists(engConfig).id;
+ return assertExists(globals.engines.get(engineId)).getProxy('QueryResult');
}
export class SqlTableTab extends BottomTab<SqlTableTabConfig> {
diff --git a/ui/src/frontend/tab_panel.ts b/ui/src/frontend/tab_panel.ts
index 46d4380..dd8ae21 100644
--- a/ui/src/frontend/tab_panel.ts
+++ b/ui/src/frontend/tab_panel.ts
@@ -67,10 +67,18 @@
const tabDropdownEntries =
globals.tabManager.tabs.filter((tab) => tab.isEphemeral === false)
.map(({content, uri}): TabDropdownEntry => {
+ // Check if the tab is already open
+ const isOpen = globals.state.tabs.openTabs.find((openTabUri) => {
+ return openTabUri === uri;
+ });
+ const clickAction = isOpen ?
+ Actions.hideTab({uri}) :
+ Actions.showTab({uri});
return {
key: uri,
title: content.getTitle(),
- onClick: () => globals.dispatch(Actions.showTab({uri})),
+ onClick: () => globals.dispatch(clickAction),
+ checked: isOpen !== undefined,
};
});
@@ -104,12 +112,12 @@
if (!exists(cs)) {
return m(EmptyState, {
className: 'pf-noselection',
- header: 'No selection',
- detail: 'Please select something',
+ header: 'Nothing selected',
+ detail: 'Selection details will appear here',
});
}
- const sectionReg = globals.tabManager.currentSelectionSections;
+ const sectionReg = globals.tabManager.detailsPanels;
const allSections = Array.from(sectionReg.values());
// Get the first "truthy" current selection section
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 5f918af..b8ff6c6 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -19,7 +19,7 @@
import {Time} from '../base/time';
import {Actions} from '../common/actions';
import {TrackCacheEntry} from '../common/track_cache';
-import {featureFlags} from '../core/feature_flags';
+import {TABS_V2_FLAG, featureFlags} from '../core/feature_flags';
import {raf} from '../core/raf_scheduler';
import {TrackTags} from '../public';
@@ -47,13 +47,6 @@
defaultValue: true,
});
-const TABS_V2 = featureFlags.register({
- id: 'tabsv2',
- name: 'Tabs V2',
- description: 'Use Tabs V2',
- defaultValue: false,
-});
-
// Checks if the mousePos is within 3px of the start or end of the
// current selected time range.
function onTimeRangeBoundary(mousePos: number): 'START'|'END'|null {
@@ -353,7 +346,7 @@
}
private renderTabPanel() {
- if (TABS_V2.get()) {
+ if (TABS_V2_FLAG.get()) {
return m(TabPanel);
} else {
return m(DetailsPanel);
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index 9625bb1..40ed061 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -215,8 +215,6 @@
USING SPAN_JOIN(RilSignalStrength PARTITIONED track_id, ScreenOn)`;
const MODEM_RIL_CHANNELS_PREAMBLE = `
- SELECT IMPORT('android.battery_stats');
-
CREATE OR REPLACE PERFETTO FUNCTION EXTRACT_KEY_VALUE(source STRING, key_name STRING) RETURNS STRING AS
SELECT SUBSTR(trimmed, INSTR(trimmed, "=")+1, INSTR(trimmed, ",") - INSTR(trimmed, "=") - 1)
FROM (SELECT SUBSTR($source, INSTR($source, $key_name)) AS trimmed);`;
@@ -943,6 +941,7 @@
WHERE track_name = "${track}"`,
groupId);
+ await e.query(`SELECT IMPORT('android.battery_stats');`);
return flatten([
query('Top App', 'battery_stats.top'),
this.addSliceTrack(
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index beafacb..e87447a 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -327,7 +327,7 @@
onShow?(): void;
}
-export interface CurrentSelectionSection {
+export interface DetailsPanel {
render(selection: Selection): m.Children;
}
@@ -402,8 +402,9 @@
// is deactivated or when the trace is unloaded.
registerTab(tab: TabDescriptor): void;
- // Register a current selection handler.
- registerCurrentSelectionSection(sel: CurrentSelectionSection): void;
+ // Register a hook into the current selection tab rendering logic that allows
+ // customization of the current selection tab content.
+ registerDetailsPanel(sel: DetailsPanel): void;
// Create a store mounted over the top of this plugin's persistent state.
mountStore<T>(migrate: Migrate<T>): Store<T>;
diff --git a/ui/src/public/utils.ts b/ui/src/public/utils.ts
index 028c3a0..cb3d814 100644
--- a/ui/src/public/utils.ts
+++ b/ui/src/public/utils.ts
@@ -17,7 +17,7 @@
import {Selection} from '../common/state';
import {BottomTab} from '../frontend/bottom_tab';
-import {CurrentSelectionSection, Tab} from '.';
+import {DetailsPanel, Tab} from '.';
export function getTrackName(args: Partial<{
name: string | null,
@@ -127,7 +127,7 @@
},
})
*/
-export class BottomTabToSCSAdapter implements CurrentSelectionSection {
+export class BottomTabToSCSAdapter implements DetailsPanel {
private oldSelection?: Selection;
private bottomTab?: BottomTab;
private attrs: BottomTabAdapterAttrs;
diff --git a/ui/src/tracks/android_log/index.ts b/ui/src/tracks/android_log/index.ts
index 28e2fe8..860890d 100644
--- a/ui/src/tracks/android_log/index.ts
+++ b/ui/src/tracks/android_log/index.ts
@@ -12,11 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import m from 'mithril';
+
import {duration, Time, time} from '../../base/time';
import {LIMIT, TrackData} from '../../common/track_data';
import {TimelineFetcher} from '../../common/track_helper';
import {checkerboardExcept} from '../../frontend/checkerboard';
import {globals} from '../../frontend/globals';
+import {LogPanel} from '../../frontend/logs_panel';
import {PanelSize} from '../../frontend/panel';
import {
EngineProxy,
@@ -158,6 +161,16 @@
track: () => new AndroidLogTrack(ctx.engine),
});
}
+
+ // Eternal tabs should always be available even if there is nothing to show
+ ctx.registerTab({
+ isEphemeral: false,
+ uri: 'android_logs',
+ content: {
+ render: () => m(LogPanel),
+ getTitle: () => 'Android Logs',
+ },
+ });
}
}
diff --git a/ui/src/tracks/chrome_critical_user_interactions/index.ts b/ui/src/tracks/chrome_critical_user_interactions/index.ts
index 29c16ca..1357d5c 100644
--- a/ui/src/tracks/chrome_critical_user_interactions/index.ts
+++ b/ui/src/tracks/chrome_critical_user_interactions/index.ts
@@ -17,13 +17,17 @@
import {Actions} from '../../common/actions';
import {SCROLLING_TRACK_GROUP} from '../../common/state';
import {OnSliceClickArgs} from '../../frontend/base_slice_track';
-import {GenericSliceDetailsTab} from '../../frontend/generic_slice_details_tab';
+import {
+ GenericSliceDetailsTab,
+ GenericSliceDetailsTabConfig,
+} from '../../frontend/generic_slice_details_tab';
import {globals} from '../../frontend/globals';
import {
NAMED_ROW,
NamedSliceTrackTypes,
} from '../../frontend/named_slice_track';
import {
+ BottomTabToSCSAdapter,
NUM,
Plugin,
PluginContext,
@@ -212,6 +216,52 @@
track: (trackCtx) => new CriticalUserInteractionTrack(
{engine: ctx.engine, trackKey: trackCtx.trackKey}),
});
+
+ ctx.registerDetailsPanel(new BottomTabToSCSAdapter({
+ tabFactory: (selection) => {
+ if (selection.kind === 'GENERIC_SLICE' &&
+ selection.detailsPanelConfig.kind === PageLoadDetailsPanel.kind) {
+ const config = selection.detailsPanelConfig.config;
+ return new PageLoadDetailsPanel({
+ config: config as GenericSliceDetailsTabConfig,
+ engine: ctx.engine,
+ uuid: uuidv4(),
+ });
+ }
+ return undefined;
+ },
+ }));
+
+ ctx.registerDetailsPanel(new BottomTabToSCSAdapter({
+ tabFactory: (selection) => {
+ if (selection.kind === 'GENERIC_SLICE' &&
+ selection.detailsPanelConfig.kind === StartupDetailsPanel.kind) {
+ const config = selection.detailsPanelConfig.config;
+ return new StartupDetailsPanel({
+ config: config as GenericSliceDetailsTabConfig,
+ engine: ctx.engine,
+ uuid: uuidv4(),
+ });
+ }
+ return undefined;
+ },
+ }));
+
+ ctx.registerDetailsPanel(new BottomTabToSCSAdapter({
+ tabFactory: (selection) => {
+ if (selection.kind === 'GENERIC_SLICE' &&
+ selection.detailsPanelConfig.kind ===
+ WebContentInteractionPanel.kind) {
+ const config = selection.detailsPanelConfig.config;
+ return new WebContentInteractionPanel({
+ config: config as GenericSliceDetailsTabConfig,
+ engine: ctx.engine,
+ uuid: uuidv4(),
+ });
+ }
+ return undefined;
+ },
+ }));
}
onActivate(ctx: PluginContext): void {
diff --git a/ui/src/tracks/chrome_scroll_jank/index.ts b/ui/src/tracks/chrome_scroll_jank/index.ts
index 38d41dd..9c8f2c3 100644
--- a/ui/src/tracks/chrome_scroll_jank/index.ts
+++ b/ui/src/tracks/chrome_scroll_jank/index.ts
@@ -21,6 +21,10 @@
import {ObjectByKey} from '../../common/state';
import {featureFlags} from '../../core/feature_flags';
import {
+ GenericSliceDetailsTabConfig,
+} from '../../frontend/generic_slice_details_tab';
+import {
+ BottomTabToSCSAdapter,
NUM,
Plugin,
PluginContext,
@@ -33,12 +37,15 @@
import {NULL_TRACK_URI} from '../null_track';
import {ChromeTasksScrollJankTrack} from './chrome_tasks_scroll_jank_track';
+import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
import {
addLatencyTracks,
EventLatencyTrack,
JANKY_LATENCY_NAME,
} from './event_latency_track';
+import {ScrollDetailsPanel} from './scroll_details_panel';
import {ScrollJankCauseMap} from './scroll_jank_cause_map';
+import {ScrollJankV3DetailsPanel} from './scroll_jank_v3_details_panel';
import {
addScrollJankV3ScrollTrack,
ScrollJankV3Track,
@@ -227,6 +234,21 @@
});
},
});
+
+ ctx.registerDetailsPanel(new BottomTabToSCSAdapter({
+ tabFactory: (selection) => {
+ if (selection.kind === 'GENERIC_SLICE' &&
+ selection.detailsPanelConfig.kind === ScrollDetailsPanel.kind) {
+ const config = selection.detailsPanelConfig.config;
+ return new ScrollDetailsPanel({
+ config: config as GenericSliceDetailsTabConfig,
+ engine: ctx.engine,
+ uuid: uuidv4(),
+ });
+ }
+ return undefined;
+ },
+ }));
}
private async addEventLatencyTrack(ctx: PluginContextTrace): Promise<void> {
@@ -307,6 +329,22 @@
return new EventLatencyTrack({engine: ctx.engine, trackKey}, baseTable);
},
});
+
+ ctx.registerDetailsPanel(new BottomTabToSCSAdapter({
+ tabFactory: (selection) => {
+ if (selection.kind === 'GENERIC_SLICE' &&
+ selection.detailsPanelConfig.kind ===
+ EventLatencySliceDetailsPanel.kind) {
+ const config = selection.detailsPanelConfig.config;
+ return new EventLatencySliceDetailsPanel({
+ config: config as GenericSliceDetailsTabConfig,
+ engine: ctx.engine,
+ uuid: uuidv4(),
+ });
+ }
+ return undefined;
+ },
+ }));
}
private async addScrollJankV3ScrollTrack(ctx: PluginContextTrace):
@@ -325,6 +363,22 @@
});
},
});
+
+ ctx.registerDetailsPanel(new BottomTabToSCSAdapter({
+ tabFactory: (selection) => {
+ if (selection.kind === 'GENERIC_SLICE' &&
+ selection.detailsPanelConfig.kind ===
+ ScrollJankV3DetailsPanel.kind) {
+ const config = selection.detailsPanelConfig.config;
+ return new ScrollJankV3DetailsPanel({
+ config: config as GenericSliceDetailsTabConfig,
+ engine: ctx.engine,
+ uuid: uuidv4(),
+ });
+ }
+ return undefined;
+ },
+ }));
}
}
diff --git a/ui/src/tracks/chrome_slices/index.ts b/ui/src/tracks/chrome_slices/index.ts
index f232352..998ebfd 100644
--- a/ui/src/tracks/chrome_slices/index.ts
+++ b/ui/src/tracks/chrome_slices/index.ts
@@ -15,6 +15,8 @@
import {BigintMath as BIMath} from '../../base/bigint_math';
import {clamp} from '../../base/math_utils';
import {Duration, duration, time} from '../../base/time';
+import {uuidv4} from '../../base/uuid';
+import {ChromeSliceDetailsTab} from '../../frontend/chrome_slice_details_tab';
import {
NAMED_ROW,
NamedSliceTrack,
@@ -27,6 +29,7 @@
} from '../../frontend/slice_track';
import {NewTrackArgs} from '../../frontend/track';
import {
+ BottomTabToSCSAdapter,
EngineProxy,
Plugin,
PluginContext,
@@ -292,6 +295,22 @@
},
});
}
+
+ ctx.registerDetailsPanel(new BottomTabToSCSAdapter({
+ tabFactory: (sel) => {
+ if (sel.kind !== 'CHROME_SLICE') {
+ return undefined;
+ }
+ return new ChromeSliceDetailsTab({
+ config: {
+ table: sel.table ?? 'slice',
+ id: sel.id,
+ },
+ engine: ctx.engine,
+ uuid: uuidv4(),
+ });
+ },
+ }));
}
}
diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts
index 4ac8a37..144ece1 100644
--- a/ui/src/tracks/counter/index.ts
+++ b/ui/src/tracks/counter/index.ts
@@ -28,6 +28,7 @@
TimelineFetcher,
} from '../../common/track_helper';
import {checkerboardExcept} from '../../frontend/checkerboard';
+import {CounterDetailsPanel} from '../../frontend/counter_panel';
import {globals} from '../../frontend/globals';
import {PanelSize} from '../../frontend/panel';
import {
@@ -630,6 +631,16 @@
await this.addCpuPerfCounterTracks(ctx);
await this.addThreadCounterTracks(ctx);
await this.addProcessCounterTracks(ctx);
+
+ ctx.registerDetailsPanel({
+ render: (sel) => {
+ if (sel.kind === 'COUNTER') {
+ return m(CounterDetailsPanel);
+ } else {
+ return undefined;
+ }
+ },
+ });
}
private async addCounterTracks(ctx: PluginContextTrace) {
diff --git a/ui/src/tracks/cpu_profile/index.ts b/ui/src/tracks/cpu_profile/index.ts
index 3c7b2f0..29f703b 100644
--- a/ui/src/tracks/cpu_profile/index.ts
+++ b/ui/src/tracks/cpu_profile/index.ts
@@ -19,6 +19,7 @@
import {colorForSample} from '../../common/colorizer';
import {TrackData} from '../../common/track_data';
import {TimelineFetcher} from '../../common/track_helper';
+import {CpuProfileDetailsPanel} from '../../frontend/cpu_profile_panel';
import {globals} from '../../frontend/globals';
import {PanelSize} from '../../frontend/panel';
import {TimeScale} from '../../frontend/time_scale';
@@ -283,6 +284,16 @@
track: () => new CpuProfileTrack(ctx.engine, utid),
});
}
+
+ ctx.registerDetailsPanel({
+ render: (sel) => {
+ if (sel.kind === 'CPU_PROFILE_SAMPLE') {
+ return m(CpuProfileDetailsPanel);
+ } else {
+ return undefined;
+ }
+ },
+ });
}
}
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 0596894..149d014 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -33,6 +33,7 @@
import {checkerboardExcept} from '../../frontend/checkerboard';
import {globals} from '../../frontend/globals';
import {PanelSize} from '../../frontend/panel';
+import {SliceDetailsPanel} from '../../frontend/slice_details_panel';
import {
EngineProxy,
Plugin,
@@ -509,6 +510,14 @@
track: ({trackKey}) => new CpuSliceTrack(ctx.engine, trackKey, cpu),
});
}
+
+ ctx.registerDetailsPanel({
+ render: (sel) => {
+ if (sel.kind === 'SLICE') {
+ return m(SliceDetailsPanel);
+ }
+ },
+ });
}
async guessCpuSizes(engine: EngineProxy): Promise<Map<number, string>> {
diff --git a/ui/src/tracks/debug/index.ts b/ui/src/tracks/debug/index.ts
index 089c69b..7beeb7a 100644
--- a/ui/src/tracks/debug/index.ts
+++ b/ui/src/tracks/debug/index.ts
@@ -13,7 +13,9 @@
// limitations under the License.
import {DEBUG_SLICE_TRACK_URI} from '../../frontend/debug_tracks';
+import {uuidv4} from '../../base/uuid';
import {
+ BottomTabToSCSAdapter,
Plugin,
PluginContext,
PluginContextTrace,
@@ -21,7 +23,9 @@
} from '../../public';
import {DebugCounterTrack} from './counter_track';
+import {DebugSliceDetailsTab} from './details_tab';
import {DebugTrackV2} from './slice_track';
+import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
export const DEBUG_COUNTER_TRACK_URI = 'perfetto.DebugCounter';
@@ -33,6 +37,22 @@
uri: DEBUG_SLICE_TRACK_URI,
track: (trackCtx) => new DebugTrackV2(ctx.engine, trackCtx),
});
+
+ ctx.registerDetailsPanel(new BottomTabToSCSAdapter({
+ tabFactory: (selection) => {
+ if (selection.kind === 'GENERIC_SLICE' &&
+ selection.detailsPanelConfig.kind === DebugSliceDetailsTab.kind) {
+ const config = selection.detailsPanelConfig.config;
+ return new DebugSliceDetailsTab({
+ config: config as GenericSliceDetailsTabConfig,
+ engine: ctx.engine,
+ uuid: uuidv4(),
+ });
+ }
+ return undefined;
+ },
+ }));
+
ctx.registerTrack({
uri: DEBUG_COUNTER_TRACK_URI,
track: (trackCtx) => new DebugCounterTrack(ctx.engine, trackCtx),
diff --git a/ui/src/tracks/flows/index.ts b/ui/src/tracks/flows/index.ts
new file mode 100644
index 0000000..523b854
--- /dev/null
+++ b/ui/src/tracks/flows/index.ts
@@ -0,0 +1,52 @@
+// 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 {
+ FlowEventsAreaSelectedPanel,
+ FlowEventsPanel,
+} from '../../frontend/flow_events_panel';
+import {globals} from '../../frontend/globals';
+import {
+ Plugin,
+ PluginContext,
+ PluginContextTrace,
+ PluginDescriptor,
+} from '../../public';
+
+class FlowsPlugin implements Plugin {
+ onActivate(_ctx: PluginContext): void {}
+
+ async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+ ctx.registerTab({
+ isEphemeral: false,
+ uri: 'perfetto.Flows#FlowEvents',
+ content: {
+ render: () => {
+ const selection = globals.state.currentSelection;
+ if (selection?.kind === 'AREA') {
+ return m(FlowEventsAreaSelectedPanel);
+ } else {
+ return m(FlowEventsPanel);
+ }
+ },
+ getTitle: () => 'Flow Events',
+ },
+ });
+ }
+}
+
+export const plugin: PluginDescriptor = {
+ pluginId: 'perfetto.Flows',
+ plugin: FlowsPlugin,
+};
diff --git a/ui/src/tracks/ftrace/index.ts b/ui/src/tracks/ftrace/index.ts
index dd1a2b7..eb5c548 100644
--- a/ui/src/tracks/ftrace/index.ts
+++ b/ui/src/tracks/ftrace/index.ts
@@ -12,11 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import m from 'mithril';
+
import {duration, Time, time} from '../../base/time';
import {colorForFtrace} from '../../common/colorizer';
import {LIMIT, TrackData} from '../../common/track_data';
import {TimelineFetcher} from '../../common/track_helper';
import {checkerboardExcept} from '../../frontend/checkerboard';
+import {FtracePanel} from '../../frontend/ftrace_panel';
import {globals} from '../../frontend/globals';
import {PanelSize} from '../../frontend/panel';
import {
@@ -150,6 +153,15 @@
},
});
}
+
+ ctx.registerTab({
+ uri: 'perfetto.FtraceRaw#FtraceEventsTab',
+ isEphemeral: false,
+ content: {
+ render: () => m(FtracePanel),
+ getTitle: () => 'Ftrace Events',
+ },
+ });
}
private async lookupCpuCores(engine: EngineProxy): Promise<number[]> {
diff --git a/ui/src/tracks/heap_profile/index.ts b/ui/src/tracks/heap_profile/index.ts
index 684404d..50c458b 100644
--- a/ui/src/tracks/heap_profile/index.ts
+++ b/ui/src/tracks/heap_profile/index.ts
@@ -22,6 +22,7 @@
OnSliceClickArgs,
OnSliceOverArgs,
} from '../../frontend/base_slice_track';
+import {FlamegraphDetailsPanel} from '../../frontend/flamegraph_panel';
import {globals} from '../../frontend/globals';
import {NewTrackArgs} from '../../frontend/track';
import {
@@ -116,10 +117,10 @@
onActivate(_ctx: PluginContext): void {}
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
const result = await ctx.engine.query(`
- select distinct(upid) from heap_profile_allocation
- union
- select distinct(upid) from heap_graph_object
- `);
+ select distinct(upid) from heap_profile_allocation
+ union
+ select distinct(upid) from heap_graph_object
+ `);
for (const it = result.iter({upid: NUM}); it.valid(); it.next()) {
const upid = it.upid;
ctx.registerTrack({
@@ -137,6 +138,16 @@
},
});
}
+
+ ctx.registerDetailsPanel({
+ render: (sel) => {
+ if (sel.kind === 'HEAP_PROFILE') {
+ return m(FlamegraphDetailsPanel);
+ } else {
+ return undefined;
+ }
+ },
+ });
}
}
diff --git a/ui/src/tracks/perf_samples_profile/index.ts b/ui/src/tracks/perf_samples_profile/index.ts
index 05ed875..a4b57e5 100644
--- a/ui/src/tracks/perf_samples_profile/index.ts
+++ b/ui/src/tracks/perf_samples_profile/index.ts
@@ -19,6 +19,7 @@
import {TrackData} from '../../common/track_data';
import {TimelineFetcher} from '../../common/track_helper';
import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph';
+import {FlamegraphDetailsPanel} from '../../frontend/flamegraph_panel';
import {globals} from '../../frontend/globals';
import {PanelSize} from '../../frontend/panel';
import {TimeScale} from '../../frontend/time_scale';
@@ -223,7 +224,7 @@
select distinct upid, pid
from perf_sample join thread using (utid) join process using (upid)
where callsite_id is not null
- `);
+ `);
for (const it = result.iter({upid: NUM, pid: NUM}); it.valid(); it.next()) {
const upid = it.upid;
const pid = it.pid;
@@ -235,6 +236,16 @@
track: () => new PerfSamplesProfileTrack(ctx.engine, upid),
});
}
+
+ ctx.registerDetailsPanel({
+ render: (sel) => {
+ if (sel.kind === 'PERF_SAMPLES') {
+ return m(FlamegraphDetailsPanel);
+ } else {
+ return undefined;
+ }
+ },
+ });
}
}
diff --git a/ui/src/tracks/pivot_table/index.ts b/ui/src/tracks/pivot_table/index.ts
new file mode 100644
index 0000000..736d2cb
--- /dev/null
+++ b/ui/src/tracks/pivot_table/index.ts
@@ -0,0 +1,45 @@
+// 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 {globals} from '../../frontend/globals';
+import {PivotTable} from '../../frontend/pivot_table';
+import {
+ Plugin,
+ PluginContext,
+ PluginContextTrace,
+ PluginDescriptor,
+} from '../../public';
+
+class PivotTablePlugin implements Plugin {
+ onActivate(_ctx: PluginContext): void {}
+
+ async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+ ctx.registerTab({
+ isEphemeral: false,
+ uri: 'perfetto.PivotTable#PivotTable',
+ content: {
+ render: () => m(PivotTable, {
+ selectionArea:
+ globals.state.nonSerializableState.pivotTable.selectionArea,
+ }),
+ getTitle: () => 'Pivot Table',
+ },
+ });
+ }
+}
+
+export const plugin: PluginDescriptor = {
+ pluginId: 'perfetto.PivotTable',
+ plugin: PivotTablePlugin,
+};
diff --git a/ui/src/tracks/screenshots/index.ts b/ui/src/tracks/screenshots/index.ts
index 254dadb..fc2c6da 100644
--- a/ui/src/tracks/screenshots/index.ts
+++ b/ui/src/tracks/screenshots/index.ts
@@ -12,11 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {uuidv4} from '../../base/uuid';
import {AddTrackArgs} from '../../common/actions';
import {
+ GenericSliceDetailsTabConfig,
+} from '../../frontend/generic_slice_details_tab';
+import {
NamedSliceTrackTypes,
} from '../../frontend/named_slice_track';
import {
+ BottomTabToSCSAdapter,
NUM,
Plugin,
PluginContext,
@@ -106,6 +111,21 @@
});
},
});
+
+ ctx.registerDetailsPanel(new BottomTabToSCSAdapter({
+ tabFactory: (selection) => {
+ if (selection.kind === 'GENERIC_SLICE' &&
+ selection.detailsPanelConfig.kind === ScreenshotTab.kind) {
+ const config = selection.detailsPanelConfig.config;
+ return new ScreenshotTab({
+ config: config as GenericSliceDetailsTabConfig,
+ engine: ctx.engine,
+ uuid: uuidv4(),
+ });
+ }
+ return undefined;
+ },
+ }));
}
}
}
diff --git a/ui/src/tracks/thread_state/index.ts b/ui/src/tracks/thread_state/index.ts
index 62ef1fe..7d3f1f0 100644
--- a/ui/src/tracks/thread_state/index.ts
+++ b/ui/src/tracks/thread_state/index.ts
@@ -27,7 +27,10 @@
import {checkerboardExcept} from '../../frontend/checkerboard';
import {globals} from '../../frontend/globals';
import {PanelSize} from '../../frontend/panel';
+import {asThreadStateSqlId} from '../../frontend/sql_types';
+import {ThreadStateTab} from '../../frontend/thread_state_tab';
import {
+ BottomTabToSCSAdapter,
EngineProxy,
Plugin,
PluginContext,
@@ -365,6 +368,21 @@
},
});
}
+
+ ctx.registerDetailsPanel(new BottomTabToSCSAdapter({
+ tabFactory: (sel) => {
+ if (sel.kind !== 'THREAD_STATE') {
+ return undefined;
+ }
+ return new ThreadStateTab({
+ config: {
+ id: asThreadStateSqlId(sel.id),
+ },
+ engine: ctx.engine,
+ uuid: uuidv4(),
+ });
+ },
+ }));
}
}