diff --git a/python/tools/check_ratchet.py b/python/tools/check_ratchet.py index 573e6a3e9a..0c845d2f3f 100755 --- a/python/tools/check_ratchet.py +++ b/python/tools/check_ratchet.py @@ -37,7 +37,7 @@ from dataclasses import dataclass EXPECTED_ANY_COUNT = 59 -EXPECTED_RUN_METRIC_COUNT = 5 +EXPECTED_RUN_METRIC_COUNT = 4 ROOT_DIR = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/ui/src/core/selection_manager.ts b/ui/src/core/selection_manager.ts index 1f930d7f3f..4fd43c28a8 100644 --- a/ui/src/core/selection_manager.ts +++ b/ui/src/core/selection_manager.ts @@ -15,18 +15,13 @@ import {assertTrue, assertUnreachable} from '../base/logging'; import { Selection, - LegacySelection, Area, SelectionOpts, SelectionManager, AreaSelectionAggregator, SqlSelectionResolver, } from '../public/selection'; -import {duration, Time, time, TimeSpan} from '../base/time'; -import { - GenericSliceDetailsTabConfig, - GenericSliceDetailsTabConfigBase, -} from '../public/details_panel'; +import {TimeSpan} from '../base/time'; import {raf} from './raf_scheduler'; import {exists, Optional} from '../base/utils'; import {TrackManagerImpl} from './track_manager'; @@ -49,7 +44,6 @@ const INCOMPLETE_SLICE_DURATION = 30_000n; // requires querying the SQL engine, which is an async operation. export class SelectionManagerImpl implements SelectionManager { private _selection: Selection = {kind: 'empty'}; - private _selectedDetails?: LegacySelectionDetails; private _aggregationManager: SelectionAggregationManager; // Incremented every time _selection changes. private readonly selectionResolvers = new Array(); @@ -177,62 +171,10 @@ export class SelectionManagerImpl implements SelectionManager { }); } - // There is no matching addLegacy as we did not support multi-single - // selection with the legacy selection system. - selectLegacy(legacySelection: LegacySelection, opts?: SelectionOpts): void { - this.setSelection( - { - kind: 'legacy', - legacySelection, - }, - opts, - ); - } - - selectGenericSlice(args: { - id: number; - sqlTableName: string; - start: time; - duration: duration; - trackUri: string; - detailsPanelConfig: { - kind: string; - config: GenericSliceDetailsTabConfigBase; - }; - }): void { - const detailsPanelConfig: GenericSliceDetailsTabConfig = { - id: args.id, - ...args.detailsPanelConfig.config, - }; - this.setSelection({ - kind: 'legacy', - legacySelection: { - kind: 'GENERIC_SLICE', - id: args.id, - sqlTableName: args.sqlTableName, - start: args.start, - duration: args.duration, - trackUri: args.trackUri, - detailsPanelConfig: { - kind: args.detailsPanelConfig.kind, - config: detailsPanelConfig, - }, - }, - }); - } - get selection(): Selection { return this._selection; } - get legacySelection(): LegacySelection | null { - return toLegacySelection(this._selection); - } - - get legacySelectionDetails(): LegacySelectionDetails | undefined { - return this._selectedDetails; - } - registerSqlSelectionResolver(resolver: SqlSelectionResolver): void { this.selectionResolvers.push(resolver); } @@ -321,19 +263,37 @@ export class SelectionManagerImpl implements SelectionManager { switch (this.selection.kind) { case 'track_event': return this.selection.trackUri; - case 'legacy': - return this.selection.legacySelection.trackUri; + // TODO(stevegolton): Handle scrolling to area and note selections. default: return undefined; } })(); - const range = this.findTimeRangeOfSelection(); + const range = this.findFocusRangeOfSelection(); this.scrollHelper.scrollTo({ time: range ? {...range} : undefined, track: uri ? {uri: uri, expandGroup: true} : undefined, }); } + // Finds the time range range that we should actually focus on - using dummy + // values for instant and incomplete slices, so we don't end up super zoomed + // in. + private findFocusRangeOfSelection(): Optional { + const sel = this.selection; + if (sel.kind === 'track_event') { + // The focus range of slices is different to that of the actual span + if (sel.dur === -1n) { + return TimeSpan.fromTimeAndDuration(sel.ts, INCOMPLETE_SLICE_DURATION); + } else if (sel.dur === 0n) { + return TimeSpan.fromTimeAndDuration(sel.ts, INSTANT_FOCUS_DURATION); + } else { + return TimeSpan.fromTimeAndDuration(sel.ts, sel.dur); + } + } else { + return this.findTimeRangeOfSelection(); + } + } + findTimeRangeOfSelection(): Optional { const sel = this.selection; if (sel.kind === 'area') { @@ -358,18 +318,6 @@ export class SelectionManagerImpl implements SelectionManager { return TimeSpan.fromTimeAndDuration(sel.ts, sel.dur); } - const legacySel = this.legacySelection; - if (!exists(legacySel)) { - return undefined; - } - - if (legacySel.kind === 'GENERIC_SLICE') { - return findTimeRangeOfSlice({ - ts: legacySel.start, - dur: legacySel.duration, - }); - } - return undefined; } @@ -377,51 +325,3 @@ export class SelectionManagerImpl implements SelectionManager { return this._aggregationManager; } } - -function toLegacySelection(selection: Selection): LegacySelection | null { - switch (selection.kind) { - case 'area': - case 'track_event': - case 'empty': - case 'note': - return null; - case 'union': - for (const child of selection.selections) { - const result = toLegacySelection(child); - if (result !== null) { - return result; - } - } - return null; - case 'legacy': - return selection.legacySelection; - default: - assertUnreachable(selection); - return null; - } -} - -// Returns the start and end points of a slice-like object If slice is instant -// or incomplete, dummy time will be returned which instead. -function findTimeRangeOfSlice(slice: {ts?: time; dur?: duration}): TimeSpan { - if (exists(slice.ts) && exists(slice.dur)) { - if (slice.dur === -1n) { - return TimeSpan.fromTimeAndDuration(slice.ts, INCOMPLETE_SLICE_DURATION); - } else if (slice.dur === 0n) { - return TimeSpan.fromTimeAndDuration(slice.ts, INSTANT_FOCUS_DURATION); - } else { - return TimeSpan.fromTimeAndDuration(slice.ts, slice.dur); - } - } else { - // TODO(primiano): unclear why we dont return undefined here. - return new TimeSpan(Time.INVALID, Time.INVALID); - } -} - -export interface LegacySelectionDetails { - ts?: time; - dur?: duration; - // Additional information for sched selection, used to draw the wakeup arrow. - wakeupTs?: time; - wakerCpu?: number; -} diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts index 3f5888a7ec..6e6b909534 100644 --- a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts +++ b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts @@ -12,20 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {OnSliceClickArgs} from '../../frontend/base_slice_track'; -import {GenericSliceDetailsTab} from '../../frontend/generic_slice_details_tab'; import {NAMED_ROW} from '../../frontend/named_slice_track'; -import {NUM, STR} from '../../trace_processor/query_result'; +import {LONG, NUM, STR} from '../../trace_processor/query_result'; import {Slice} from '../../public/track'; import { - CustomSqlDetailsPanelConfig, CustomSqlImportConfig, CustomSqlTableDefConfig, CustomSqlTableSliceTrack, } from '../../frontend/tracks/custom_sql_table_slice_track'; -import {PageLoadDetailsPanel} from './page_load_details_panel'; -import {StartupDetailsPanel} from './startup_details_panel'; -import {WebContentInteractionPanel} from './web_content_interaction_details_panel'; +import {TrackEventDetails} from '../../public/selection'; +import {Duration, Time} from '../../base/time'; export const CRITICAL_USER_INTERACTIONS_KIND = 'org.chromium.CriticalUserInteraction.track'; @@ -42,28 +38,6 @@ export interface CriticalUserInteractionSlice extends Slice { type: string; } -enum CriticalUserInteractionType { - UNKNOWN = 'Unknown', - PAGE_LOAD = 'chrome_page_loads', - STARTUP = 'chrome_startups', - WEB_CONTENT_INTERACTION = 'chrome_web_content_interactions', -} - -function convertToCriticalUserInteractionType( - cujType: string, -): CriticalUserInteractionType { - switch (cujType) { - case CriticalUserInteractionType.PAGE_LOAD: - return CriticalUserInteractionType.PAGE_LOAD; - case CriticalUserInteractionType.STARTUP: - return CriticalUserInteractionType.STARTUP; - case CriticalUserInteractionType.WEB_CONTENT_INTERACTION: - return CriticalUserInteractionType.WEB_CONTENT_INTERACTION; - default: - return CriticalUserInteractionType.UNKNOWN; - } -} - export class CriticalUserInteractionTrack extends CustomSqlTableSliceTrack { static readonly kind = `/critical_user_interactions`; @@ -84,64 +58,34 @@ export class CriticalUserInteractionTrack extends CustomSqlTableSliceTrack { }; } - getDetailsPanel( - args: OnSliceClickArgs, - ): CustomSqlDetailsPanelConfig { - let detailsPanel = { - kind: GenericSliceDetailsTab.kind, - config: { - sqlTableName: this.tableName, - title: 'Chrome Interaction', - }, - }; + async getSelectionDetails( + id: number, + ): Promise { + const query = ` + SELECT + ts, + dur, + type + FROM (${this.getSqlSource()}) + WHERE id = ${id} + `; - switch (convertToCriticalUserInteractionType(args.slice.type)) { - case CriticalUserInteractionType.PAGE_LOAD: - detailsPanel = { - kind: PageLoadDetailsPanel.kind, - config: { - sqlTableName: this.tableName, - title: 'Chrome Page Load', - }, - }; - break; - case CriticalUserInteractionType.STARTUP: - detailsPanel = { - kind: StartupDetailsPanel.kind, - config: { - sqlTableName: this.tableName, - title: 'Chrome Startup', - }, - }; - break; - case CriticalUserInteractionType.WEB_CONTENT_INTERACTION: - detailsPanel = { - kind: WebContentInteractionPanel.kind, - config: { - sqlTableName: this.tableName, - title: 'Chrome Web Content Interaction', - }, - }; - break; - default: - break; + const result = await this.engine.query(query); + if (result.numRows() === 0) { + return undefined; } - return detailsPanel; - } - onSliceClick(args: OnSliceClickArgs) { - const detailsPanelConfig = this.getDetailsPanel(args); - this.trace.selection.selectGenericSlice({ - id: args.slice.scopedId, - sqlTableName: this.tableName, - start: args.slice.ts, - duration: args.slice.dur, - trackUri: this.uri, - detailsPanelConfig: { - kind: detailsPanelConfig.kind, - config: detailsPanelConfig.config, - }, + const row = result.iter({ + ts: LONG, + dur: LONG, + type: STR, }); + + return { + ts: Time.fromRaw(row.ts), + dur: Duration.fromRaw(row.dur), + interactionType: row.type, + }; } getSqlImports(): CustomSqlImportConfig { diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts index 23560c956a..1660f10a5e 100644 --- a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts +++ b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts @@ -12,9 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {v4 as uuidv4} from 'uuid'; -import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab'; -import {BottomTabToSCSAdapter} from '../../public/utils'; import {Trace} from '../../public/trace'; import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin'; import {PageLoadDetailsPanel} from './page_load_details_panel'; @@ -22,6 +19,8 @@ import {StartupDetailsPanel} from './startup_details_panel'; import {WebContentInteractionPanel} from './web_content_interaction_details_panel'; import {CriticalUserInteractionTrack} from './critical_user_interaction_track'; import {TrackNode} from '../../public/workspace'; +import {TrackEventSelection} from '../../public/selection'; +import {GenericSliceDetailsTab} from '../../frontend/generic_slice_details_tab'; class CriticalUserInteractionPlugin implements PerfettoPlugin { async onTraceLoad(ctx: Trace): Promise { @@ -48,65 +47,24 @@ class CriticalUserInteractionPlugin implements PerfettoPlugin { trace: ctx, uri: CriticalUserInteractionTrack.kind, }), + detailsPanel: (sel: TrackEventSelection) => { + switch (sel.interactionType) { + case 'chrome_page_loads': + return new PageLoadDetailsPanel(ctx, sel.eventId); + case 'chrome_startups': + return new StartupDetailsPanel(ctx, sel.eventId); + case 'chrome_web_content_interactions': + return new WebContentInteractionPanel(ctx, sel.eventId); + default: + return new GenericSliceDetailsTab( + ctx, + 'chrome_interactions', + sel.eventId, + 'Chrome Interaction', + ); + } + }, }); - - ctx.tabs.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, - trace: ctx, - uuid: uuidv4(), - }); - } - return undefined; - }, - }), - ); - - ctx.tabs.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, - trace: ctx, - uuid: uuidv4(), - }); - } - return undefined; - }, - }), - ); - - ctx.tabs.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, - trace: ctx, - uuid: uuidv4(), - }); - } - return undefined; - }, - }), - ); } } diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts b/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts index e01e4a8ffd..e2d0b54006 100644 --- a/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts +++ b/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts @@ -13,29 +13,24 @@ // limitations under the License. import m from 'mithril'; -import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab'; -import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab'; import { Details, DetailsSchema, } from '../../frontend/widgets/sql/details/details'; import {DetailsShell} from '../../widgets/details_shell'; import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout'; +import {TrackEventDetailsPanel} from '../../public/details_panel'; +import {Trace} from '../../public/trace'; import d = DetailsSchema; -export class PageLoadDetailsPanel extends BottomTab { - static readonly kind = 'org.perfetto.PageLoadDetailsPanel'; +export class PageLoadDetailsPanel implements TrackEventDetailsPanel { private data: Details; - static create( - args: NewBottomTabArgs, - ): PageLoadDetailsPanel { - return new PageLoadDetailsPanel(args); - } - - constructor(args: NewBottomTabArgs) { - super(args); - this.data = new Details(this.trace, 'chrome_page_loads', this.config.id, { + constructor( + private readonly trace: Trace, + id: number, + ) { + this.data = new Details(this.trace, 'chrome_page_loads', id, { 'Navigation start': d.Timestamp('navigation_start_ts'), 'FCP event': d.Timestamp('fcp_ts'), 'FCP': d.Interval('navigation_start_ts', 'fcp'), @@ -65,21 +60,13 @@ export class PageLoadDetailsPanel extends BottomTab { - static readonly kind = 'org.perfetto.StartupDetailsPanel'; - private loaded = false; - private data: Data | undefined; +export class StartupDetailsPanel implements TrackEventDetailsPanel { + private data?: Data; - static create( - args: NewBottomTabArgs, - ): StartupDetailsPanel { - return new StartupDetailsPanel(args); - } - - constructor(args: NewBottomTabArgs) { - super(args); - this.loadData(); - } + constructor( + private readonly trace: Trace, + private readonly id: number, + ) {} - private async loadData() { - const queryResult = await this.engine.query(` + async load() { + const queryResult = await this.trace.engine.query(` SELECT activity_id AS startupId, name, @@ -64,7 +56,7 @@ export class StartupDetailsPanel extends BottomTab launch_cause AS launchCause, browser_upid AS upid FROM chrome_startups - WHERE id = ${this.config.id}; + WHERE id = ${this.id}; `); const iter = queryResult.firstRow({ @@ -87,8 +79,6 @@ export class StartupDetailsPanel extends BottomTab if (iter.launchCause) { this.data.launchCause = iter.launchCause; } - - this.loaded = true; } private getDetailsDictionary() { @@ -106,20 +96,20 @@ export class StartupDetailsPanel extends BottomTab } details['SQL ID'] = m(SqlRef, { table: 'chrome_startups', - id: this.config.id, + id: this.id, }); return details; } - viewTab() { - if (this.isLoading()) { + render() { + if (!this.data) { return m('h2', 'Loading'); } return m( DetailsShell, { - title: this.getTitle(), + title: 'Chrome Startup', }, m( GridLayout, @@ -134,12 +124,4 @@ export class StartupDetailsPanel extends BottomTab ), ); } - - getTitle(): string { - return this.config.title; - } - - isLoading() { - return !this.loaded; - } } diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts b/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts index cb6a7fb5dd..25e49aa48a 100644 --- a/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts +++ b/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts @@ -28,8 +28,6 @@ import m from 'mithril'; import {duration, Time, time} from '../../base/time'; -import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab'; -import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab'; import {asUpid, Upid} from '../../trace_processor/sql_utils/core_types'; import {DurationWidget} from '../../frontend/widgets/duration'; import {Timestamp} from '../../frontend/widgets/timestamp'; @@ -39,6 +37,8 @@ import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout'; import {Section} from '../../widgets/section'; import {SqlRef} from '../../widgets/sql_ref'; import {dictToTreeNodes, Tree} from '../../widgets/tree'; +import {TrackEventDetailsPanel} from '../../public/details_panel'; +import {Trace} from '../../public/trace'; interface Data { ts: time; @@ -48,24 +48,16 @@ interface Data { upid: Upid; } -export class WebContentInteractionPanel extends BottomTab { - static readonly kind = 'org.perfetto.WebContentInteractionPanel'; - private loaded = false; - private data: Data | undefined; +export class WebContentInteractionPanel implements TrackEventDetailsPanel { + private data?: Data; - static create( - args: NewBottomTabArgs, - ): WebContentInteractionPanel { - return new WebContentInteractionPanel(args); - } - - constructor(args: NewBottomTabArgs) { - super(args); - this.loadData(); - } + constructor( + private readonly trace: Trace, + private readonly id: number, + ) {} - private async loadData() { - const queryResult = await this.engine.query(` + async load() { + const queryResult = await this.trace.engine.query(` SELECT ts, dur, @@ -73,7 +65,7 @@ export class WebContentInteractionPanel extends BottomTab; - - private constructor() { - this.tracks = {}; - } - - public static getInstance(): ScrollJankPluginState { - if (!ScrollJankPluginState.instance) { - ScrollJankPluginState.instance = new ScrollJankPluginState(); - } - - return ScrollJankPluginState.instance; - } - - public registerTrack(args: { - kind: string; - trackUri: string; - tableName: string; - detailsPanelConfig: CustomSqlDetailsPanelConfig; - }): void { - this.tracks[args.kind] = { - key: args.trackUri, - sqlTableName: args.tableName, - detailsPanelConfig: args.detailsPanelConfig, - }; - } - - public unregisterTrack(kind: string): void { - delete this.tracks[kind]; - } - - public getTrack(kind: string): ScrollJankTrackSpec | undefined { - return this.tracks[kind]; - } -} diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts index 2d629ea2b9..ae6b128587 100644 --- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts +++ b/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts @@ -13,10 +13,8 @@ // limitations under the License. import m from 'mithril'; -import {Duration, duration, time} from '../../base/time'; +import {Duration, duration, Time, time} from '../../base/time'; import {raf} from '../../core/raf_scheduler'; -import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab'; -import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab'; import {hasArgs, renderArguments} from '../../frontend/slice_args'; import {renderDetails} from '../../frontend/slice_details'; import { @@ -37,7 +35,7 @@ import { widgetColumn, } from '../../frontend/tables/table'; import {TreeTable, TreeTableAttrs} from '../../frontend/widgets/treetable'; -import {NUM, STR} from '../../trace_processor/query_result'; +import {LONG, NUM, STR} from '../../trace_processor/query_result'; import {DetailsShell} from '../../widgets/details_shell'; import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout'; import {Section} from '../../widgets/section'; @@ -51,13 +49,10 @@ import { getScrollJankCauseStage, } from './scroll_jank_cause_link_utils'; import {ScrollJankCauseMap} from './scroll_jank_cause_map'; -import { - getScrollJankSlices, - getSliceForTrack, - ScrollJankSlice, -} from './scroll_jank_slice'; import {sliceRef} from '../../frontend/widgets/slice'; -import {SCROLL_JANK_V3_TRACK_KIND} from '../../public/track_kinds'; +import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils'; +import {TrackEventDetailsPanel} from '../../public/details_panel'; +import {Trace} from '../../public/trace'; // Given a node in the slice tree, return a path from root to it. function getPath(slice: SliceTreeNode): string[] { @@ -104,15 +99,17 @@ function durationDelta(value: duration, base?: duration): string { return `${delta > 0 ? '+' : ''}${Duration.humanise(delta)}`; } -export class EventLatencySliceDetailsPanel extends BottomTab { - static readonly kind = 'dev.perfetto.EventLatencySliceDetailsPanel'; - - private loaded = false; +export class EventLatencySliceDetailsPanel implements TrackEventDetailsPanel { private name = ''; private topEventLatencyId: SliceSqlId | undefined = undefined; private sliceDetails?: SliceDetails; - private jankySlice?: ScrollJankSlice; + private jankySlice?: { + ts: time; + dur: duration; + id: number; + causeOfJank: string; + }; // Whether this stage has caused jank. This is also true for top level // EventLatency slices where a descendant is a cause of jank. @@ -132,31 +129,24 @@ export class EventLatencySliceDetailsPanel extends BottomTab; - static create( - args: NewBottomTabArgs, - ): EventLatencySliceDetailsPanel { - return new EventLatencySliceDetailsPanel(args); - } - - constructor(args: NewBottomTabArgs) { - super(args); - + constructor( + private readonly trace: Trace, + private readonly id: number, + ) { this.tracksByTrackId = new Map(); this.trace.tracks.getAllTracks().forEach((td) => { td.tags?.trackIds?.forEach((trackId) => { this.tracksByTrackId.set(trackId, td.uri); }); }); - - this.loadData(); } - async loadData() { - const queryResult = await this.engine.query(` + async load() { + const queryResult = await this.trace.engine.query(` SELECT name - FROM ${this.config.sqlTableName} - WHERE id = ${this.config.id} + FROM slice + WHERE id = ${this.id} `); const iter = queryResult.firstRow({ @@ -169,14 +159,12 @@ export class EventLatencySliceDetailsPanel extends BottomTab 0) { - this.jankySlice = possibleSlices[0]; + const it = ( + await this.trace.engine.query(` + SELECT ts, dur, id, cause_of_jank as causeOfJank + FROM chrome_janky_frame_presentation_intervals + WHERE event_latency_id = ${this.topEventLatencyId}`) + ).iter({ + id: NUM, + ts: LONG, + dur: LONG, + causeOfJank: STR, + }); + + if (it.valid()) { + this.jankySlice = { + id: it.id, + ts: Time.fromRaw(it.ts), + dur: Duration.fromRaw(it.dur), + causeOfJank: it.causeOfJank, + }; } } @@ -214,7 +213,7 @@ export class EventLatencySliceDetailsPanel extends BottomTab 0) { this.prevEventLatencyBreakdown = await getDescendantSliceTree( - this.engine, + this.trace.engine, prevEventLatency[0].id, ); } - const nextEventLatency = await getSliceFromConstraints(this.engine, { + const nextEventLatency = await getSliceFromConstraints(this.trace.engine, { filters: [ `name = 'EventLatency'`, `id > ${this.topEventLatencyId}`, @@ -291,7 +290,7 @@ export class EventLatencySliceDetailsPanel extends BottomTab 0) { this.nextEventLatencyBreakdown = await getDescendantSliceTree( - this.engine, + this.trace.engine, nextEventLatency[0].id, ); } @@ -381,7 +380,7 @@ export class EventLatencySliceDetailsPanel extends BottomTab { let eventLatencyId = -1; if (!this.sliceDetails) return eventLatencyId; - const queryResult = await this.engine.query(` + const queryResult = await this.trace.engine.query(` SELECT id FROM ancestor_slice(${this.sliceDetails.id}) @@ -415,16 +414,15 @@ export class EventLatencySliceDetailsPanel extends BottomTab { - await super.onDestroy(); - ScrollJankPluginState.getInstance().unregisterTrack( - CHROME_EVENT_LATENCY_TRACK_KIND, - ); } getSqlSource(): string { return `SELECT * FROM ${this.baseTable}`; } - getDetailsPanel(): CustomSqlDetailsPanelConfig { - return { - kind: EventLatencySliceDetailsPanel.kind, - config: {title: '', sqlTableName: this.tableName}, - }; - } - getSqlDataSource(): CustomSqlTableDefConfig { return { sqlTableName: this.baseTable, @@ -76,22 +50,6 @@ export class EventLatencyTrack extends CustomSqlTableSliceTrack { } } - onUpdatedSlices(slices: Slice[]) { - for (const slice of slices) { - const currentSelection = this.trace.selection.legacySelection; - const isSelected = - exists(currentSelection) && - currentSelection.kind === 'GENERIC_SLICE' && - currentSelection.id !== undefined && - currentSelection.id === slice.id; - - const highlighted = globals.state.highlightedSliceId === slice.id; - const hasFocus = highlighted || isSelected; - slice.isHighlighted = !!hasFocus; - } - super.onUpdatedSlices(slices); - } - // At the moment we will just display the slice details. However, on select, // this behavior should be customized to show jank-related data. } diff --git a/ui/src/core_plugins/chrome_scroll_jank/index.ts b/ui/src/core_plugins/chrome_scroll_jank/index.ts index e67921f383..ab0dde0ac1 100644 --- a/ui/src/core_plugins/chrome_scroll_jank/index.ts +++ b/ui/src/core_plugins/chrome_scroll_jank/index.ts @@ -12,24 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {v4 as uuidv4} from 'uuid'; import {uuidv4Sql} from '../../base/uuid'; import {generateSqlWithInternalLayout} from '../../common/internal_layout_utils'; import {featureFlags} from '../../core/feature_flags'; -import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab'; -import {BottomTabToSCSAdapter} from '../../public/utils'; -import { - CHROME_EVENT_LATENCY_TRACK_KIND, - CHROME_TOPLEVEL_SCROLLS_KIND, - CHROME_SCROLL_JANK_TRACK_KIND, - SCROLL_JANK_V3_TRACK_KIND, -} from '../../public/track_kinds'; -import {NUM} from '../../trace_processor/query_result'; import {Trace} from '../../public/trace'; import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin'; -import {Engine} from '../../trace_processor/engine'; -import {ChromeTasksScrollJankTrack} from './chrome_tasks_scroll_jank_track'; -import {ENABLE_CHROME_SCROLL_JANK_PLUGIN} from './common'; import {EventLatencySliceDetailsPanel} from './event_latency_details_panel'; import {EventLatencyTrack, JANKY_LATENCY_NAME} from './event_latency_track'; import {ScrollDetailsPanel} from './scroll_details_panel'; @@ -38,9 +25,6 @@ import {ScrollJankV3Track} from './scroll_jank_v3_track'; import {TopLevelScrollTrack} from './scroll_track'; import {ScrollJankCauseMap} from './scroll_jank_cause_map'; import {TrackNode} from '../../public/workspace'; -import {getOrCreateGroupForThread} from '../../public/standard_groups'; -import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab'; -import {ThreadSliceDetailsPanel} from '../../frontend/thread_slice_details_tab'; const ENABLE_SCROLL_JANK_PLUGIN_V2 = featureFlags.register({ id: 'enableScrollJankPluginV2', @@ -51,38 +35,6 @@ const ENABLE_SCROLL_JANK_PLUGIN_V2 = featureFlags.register({ class ChromeScrollJankPlugin implements PerfettoPlugin { async onTraceLoad(ctx: Trace): Promise { - if (ENABLE_CHROME_SCROLL_JANK_PLUGIN.get()) { - await this.addChromeScrollJankTrack(ctx); - - if (!(await isChromeTrace(ctx.engine))) { - return; - } - - // Initialise the chrome_tasks_delaying_input_processing table. It will be - // used in the tracks above. - await ctx.engine.query(` - INCLUDE PERFETTO MODULE deprecated.v42.common.slices; - SELECT RUN_METRIC( - 'chrome/chrome_tasks_delaying_input_processing.sql', - 'duration_causing_jank_ms', - /* duration_causing_jank_ms = */ '8');`); - - const query = ` - select - s1.full_name, - s1.duration_ms, - s1.slice_id, - s1.thread_dur_ms, - s2.id, - s2.ts, - s2.dur, - s2.track_id - from chrome_tasks_delaying_input_processing s1 - join slice s2 on s1.slice_id=s2.id - `; - addQueryResultsTab(ctx, {query, title: 'Scroll Jank: long tasks'}); - } - if (ENABLE_SCROLL_JANK_PLUGIN_V2.get()) { const group = new TrackNode({ title: 'Chrome Scroll Jank', @@ -98,46 +50,6 @@ class ChromeScrollJankPlugin implements PerfettoPlugin { } } - private async addChromeScrollJankTrack(ctx: Trace): Promise { - const queryResult = await ctx.engine.query(` - select - utid, - upid - from thread - where name='CrBrowserMain' - `); - - if (queryResult.numRows() === 0) { - return; - } - - const it = queryResult.firstRow({ - utid: NUM, - upid: NUM, - }); - - const {upid, utid} = it; - const uri = 'perfetto.ChromeScrollJank'; - const title = 'Scroll Jank causes - long tasks'; - ctx.tracks.registerTrack({ - uri, - title, - tags: { - kind: CHROME_SCROLL_JANK_TRACK_KIND, - upid, - utid, - }, - track: new ChromeTasksScrollJankTrack({ - trace: ctx, - uri, - }), - detailsPanel: () => new ThreadSliceDetailsPanel(ctx, 'slice'), - }); - const group = getOrCreateGroupForThread(ctx.workspace, utid); - const track = new TrackNode({uri, title}); - group.addChildInOrder(track); - } - private async addTopLevelScrollTrack( ctx: Trace, group: TrackNode, @@ -153,36 +65,17 @@ class ChromeScrollJankPlugin implements PerfettoPlugin { ctx.tracks.registerTrack({ uri, title, - tags: { - kind: CHROME_TOPLEVEL_SCROLLS_KIND, - }, track: new TopLevelScrollTrack({ trace: ctx, uri, }), + detailsPanel: (sel) => { + return new ScrollDetailsPanel(ctx, sel.eventId); + }, }); const track = new TrackNode({uri, title}); group.addChildInOrder(track); - - ctx.tabs.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, - trace: ctx, - uuid: uuidv4(), - }); - } - return undefined; - }, - }), - ); } private async addEventLatencyTrack( @@ -291,34 +184,14 @@ class ChromeScrollJankPlugin implements PerfettoPlugin { ctx.tracks.registerTrack({ uri, title, - tags: { - kind: CHROME_EVENT_LATENCY_TRACK_KIND, - }, track: new EventLatencyTrack({trace: ctx, uri}, baseTable), + detailsPanel: (sel) => { + return new EventLatencySliceDetailsPanel(ctx, sel.eventId); + }, }); const track = new TrackNode({uri, title}); group.addChildInOrder(track); - - ctx.tabs.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, - trace: ctx, - uuid: uuidv4(), - }); - } - return undefined; - }, - }), - ); } private async addScrollJankV3ScrollTrack( @@ -335,54 +208,18 @@ class ChromeScrollJankPlugin implements PerfettoPlugin { ctx.tracks.registerTrack({ uri, title, - tags: { - kind: SCROLL_JANK_V3_TRACK_KIND, - }, track: new ScrollJankV3Track({ trace: ctx, uri, }), + detailsPanel: (sel) => new ScrollJankV3DetailsPanel(ctx, sel.eventId), }); const track = new TrackNode({uri, title}); group.addChildInOrder(track); - - ctx.tabs.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, - trace: ctx, - uuid: uuidv4(), - }); - } - return undefined; - }, - }), - ); } } -async function isChromeTrace(engine: Engine) { - const queryResult = await engine.query(` - select utid, upid - from thread - where name='CrBrowserMain' - `); - - const it = queryResult.iter({ - utid: NUM, - upid: NUM, - }); - - return it.valid(); -} - export const plugin: PluginDescriptor = { pluginId: 'perfetto.ChromeScrollJank', plugin: ChromeScrollJankPlugin, diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts index c184ba7d6e..30ab521df0 100644 --- a/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts +++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts @@ -15,19 +15,21 @@ import m from 'mithril'; import {duration, Time, time} from '../../base/time'; import {exists} from '../../base/utils'; -import {raf} from '../../core/raf_scheduler'; -import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab'; -import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab'; import { ColumnDescriptor, - numberColumn, Table, TableData, widgetColumn, } from '../../frontend/tables/table'; import {DurationWidget} from '../../frontend/widgets/duration'; import {Timestamp} from '../../frontend/widgets/timestamp'; -import {LONG, NUM, STR} from '../../trace_processor/query_result'; +import { + LONG, + LONG_NULL, + NUM, + NUM_NULL, + STR, +} from '../../trace_processor/query_result'; import {DetailsShell} from '../../widgets/details_shell'; import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout'; import {Section} from '../../widgets/section'; @@ -41,12 +43,9 @@ import { getPredictorJankDeltas, getPresentedScrollDeltas, } from './scroll_delta_graph'; -import { - getScrollJankSlices, - getSliceForTrack, - ScrollJankSlice, -} from './scroll_jank_slice'; -import {SCROLL_JANK_V3_TRACK_KIND} from '../../public/track_kinds'; +import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils'; +import {TrackEventDetailsPanel} from '../../public/details_panel'; +import {Trace} from '../../public/trace'; interface Data { // Scroll ID. @@ -71,32 +70,27 @@ interface Metrics { interface JankSliceDetails { cause: string; - jankSlice: ScrollJankSlice; - delayDur: duration; - delayVsync: number; + id: number; + ts: time; + dur?: duration; + delayVsync?: number; } -export class ScrollDetailsPanel extends BottomTab { - static readonly kind = 'org.perfetto.ScrollDetailsPanel'; - loaded = false; - data: Data | undefined; - metrics: Metrics = {}; - orderedJankSlices: JankSliceDetails[] = []; - scrollDeltas: m.Child; - - static create( - args: NewBottomTabArgs, - ): ScrollDetailsPanel { - return new ScrollDetailsPanel(args); - } +export class ScrollDetailsPanel implements TrackEventDetailsPanel { + private data?: Data; + private metrics: Metrics = {}; + private orderedJankSlices: JankSliceDetails[] = []; - constructor(args: NewBottomTabArgs) { - super(args); - this.loadData(); - } + // TODO(altimin): Don't store Mithril vnodes between render cycles. + private scrollDeltas: m.Child; + + constructor( + private readonly trace: Trace, + private readonly id: number, + ) {} - private async loadData() { - const queryResult = await this.engine.query(` + async load() { + const queryResult = await this.trace.engine.query(` WITH scrolls AS ( SELECT id, @@ -107,7 +101,7 @@ export class ScrollDetailsPanel extends BottomTab THEN gesture_scroll_begin_ts + dur ELSE ts + dur END AS end_ts - FROM chrome_scrolls WHERE id = ${this.config.id}) + FROM chrome_scrolls WHERE id = ${this.id}) SELECT id, start_ts AS ts, @@ -126,8 +120,6 @@ export class ScrollDetailsPanel extends BottomTab }; await this.loadMetrics(); - this.loaded = true; - raf.scheduleFullRedraw(); } private async loadMetrics() { @@ -139,7 +131,7 @@ export class ScrollDetailsPanel extends BottomTab private async loadInputEventCount() { if (exists(this.data)) { - const queryResult = await this.engine.query(` + const queryResult = await this.trace.engine.query(` SELECT COUNT(*) AS inputEventCount FROM slice s @@ -159,7 +151,7 @@ export class ScrollDetailsPanel extends BottomTab private async loadFrameStats() { if (exists(this.data)) { - const queryResult = await this.engine.query(` + const queryResult = await this.trace.engine.query(` SELECT IFNULL(frame_count, 0) AS frameCount, IFNULL(missed_vsyncs, 0) AS missedVsyncs, @@ -190,39 +182,36 @@ export class ScrollDetailsPanel extends BottomTab private async loadDelayData() { if (exists(this.data)) { - const queryResult = await this.engine.query(` + const queryResult = await this.trace.engine.query(` SELECT + id, + ts, + dur, IFNULL(sub_cause_of_jank, IFNULL(cause_of_jank, 'Unknown')) AS cause, - IFNULL(event_latency_id, 0) AS eventLatencyId, - IFNULL(dur, 0) AS delayDur, - IFNULL(delayed_frame_count, 0) AS delayVsync + event_latency_id AS eventLatencyId, + delayed_frame_count AS delayVsync FROM chrome_janky_frame_presentation_intervals s WHERE s.ts >= ${this.data.ts} AND s.ts + s.dur <= ${this.data.ts + this.data.dur} - ORDER by delayDur DESC; + ORDER by dur DESC; `); - const iter = queryResult.iter({ + const it = queryResult.iter({ + id: NUM, + ts: LONG, + dur: LONG_NULL, cause: STR, - eventLatencyId: NUM, - delayDur: LONG, - delayVsync: NUM, + eventLatencyId: NUM_NULL, + delayVsync: NUM_NULL, }); - for (; iter.valid(); iter.next()) { - if (iter.delayDur <= 0) { - break; - } - const jankSlices = await getScrollJankSlices( - this.engine, - iter.eventLatencyId, - ); - + for (; it.valid(); it.next()) { this.orderedJankSlices.push({ - cause: iter.cause, - jankSlice: jankSlices[0], - delayDur: iter.delayDur, - delayVsync: iter.delayVsync, + id: it.id, + ts: Time.fromRaw(it.ts), + dur: it.dur ?? undefined, + cause: it.cause, + delayVsync: it.delayVsync ?? undefined, }); } } @@ -230,17 +219,20 @@ export class ScrollDetailsPanel extends BottomTab private async loadScrollOffsets() { if (exists(this.data)) { - const inputDeltas = await getInputScrollDeltas(this.engine, this.data.id); + const inputDeltas = await getInputScrollDeltas( + this.trace.engine, + this.data.id, + ); const presentedDeltas = await getPresentedScrollDeltas( - this.engine, + this.trace.engine, this.data.id, ); const predictorDeltas = await getPredictorJankDeltas( - this.engine, + this.trace.engine, this.data.id, ); const jankIntervals = await getJankIntervals( - this.engine, + this.trace.engine, this.data.ts, this.data.dur, ); @@ -310,31 +302,27 @@ export class ScrollDetailsPanel extends BottomTab private getDelayTable(): m.Child { if (this.orderedJankSlices.length > 0) { - interface DelayData { - jankLink: m.Child; - dur: m.Child; - delayedVSyncs: number; - } - - const columns: ColumnDescriptor[] = [ - widgetColumn('Cause', (x) => x.jankLink), - widgetColumn('Duration', (x) => x.dur), - numberColumn('Delayed Vsyncs', (x) => x.delayedVSyncs), + const columns: ColumnDescriptor[] = [ + widgetColumn('Cause', (jankSlice) => + renderSliceRef({ + trace: this.trace, + id: jankSlice.id, + trackUri: JANKS_TRACK_URI, + title: jankSlice.cause, + }), + ), + widgetColumn('Duration', (jankSlice) => + jankSlice.dur !== undefined + ? m(DurationWidget, {dur: jankSlice.dur}) + : 'NULL', + ), + widgetColumn( + 'Delayed Vsyncs', + (jankSlice) => jankSlice.delayVsync, + ), ]; - const data: DelayData[] = []; - for (const jankSlice of this.orderedJankSlices) { - data.push({ - jankLink: getSliceForTrack( - jankSlice.jankSlice, - SCROLL_JANK_V3_TRACK_KIND, - jankSlice.cause, - ), - dur: m(DurationWidget, {dur: jankSlice.delayDur}), - delayedVSyncs: jankSlice.delayVsync, - }); - } - const tableData = new TableData(data); + const tableData = new TableData(this.orderedJankSlices); return m(Table, { data: tableData, @@ -391,8 +379,8 @@ export class ScrollDetailsPanel extends BottomTab ); } - viewTab() { - if (this.isLoading() || this.data == undefined) { + render() { + if (this.data == undefined) { return m('h2', 'Loading'); } @@ -400,13 +388,13 @@ export class ScrollDetailsPanel extends BottomTab 'Scroll ID': this.data.id, 'Start time': m(Timestamp, {ts: this.data.ts}), 'Duration': m(DurationWidget, {dur: this.data.dur}), - 'SQL ID': m(SqlRef, {table: 'chrome_scrolls', id: this.config.id}), + 'SQL ID': m(SqlRef, {table: 'chrome_scrolls', id: this.id}), }); return m( DetailsShell, { - title: this.getTitle(), + title: 'Scroll', }, m( GridLayout, @@ -437,12 +425,4 @@ export class ScrollDetailsPanel extends BottomTab ), ); } - - getTitle(): string { - return this.config.title; - } - - isLoading() { - return !this.loaded; - } } diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts deleted file mode 100644 index 2b3e33c36a..0000000000 --- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts +++ /dev/null @@ -1,214 +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 m from 'mithril'; -import {Icons} from '../../base/semantic_icons'; -import {duration, time, Time} from '../../base/time'; -import {globals} from '../../frontend/globals'; -import {SliceSqlId} from '../../trace_processor/sql_utils/core_types'; -import {Engine} from '../../trace_processor/engine'; -import {LONG, NUM} from '../../trace_processor/query_result'; -import { - constraintsToQuerySuffix, - SQLConstraints, -} from '../../trace_processor/sql_utils'; -import {Anchor} from '../../widgets/anchor'; -import {ScrollJankPluginState, ScrollJankTrackSpec} from './common'; -import { - CHROME_EVENT_LATENCY_TRACK_KIND, - SCROLL_JANK_V3_TRACK_KIND, -} from '../../public/track_kinds'; -import {scrollTo} from '../../public/scroll_helper'; - -interface BasicSlice { - // ID of slice. - sliceId: number; - // Timestamp of the beginning of this slice in nanoseconds. - ts: time; - // Duration of this slice in nanoseconds. - dur: duration; -} - -async function getSlicesFromTrack( - engine: Engine, - track: ScrollJankTrackSpec, - constraints: SQLConstraints, -): Promise { - const query = await engine.query(` - SELECT - id AS sliceId, - ts, - dur AS dur - FROM ${track.sqlTableName} - ${constraintsToQuerySuffix(constraints)}`); - const it = query.iter({ - sliceId: NUM, - ts: LONG, - dur: LONG, - }); - - const result: BasicSlice[] = []; - for (; it.valid(); it.next()) { - result.push({ - sliceId: it.sliceId as number, - ts: Time.fromRaw(it.ts), - dur: it.dur, - }); - } - return result; -} - -export type ScrollJankSlice = BasicSlice; -export async function getScrollJankSlices( - engine: Engine, - id: number, -): Promise { - const track = ScrollJankPluginState.getInstance().getTrack( - SCROLL_JANK_V3_TRACK_KIND, - ); - if (track == undefined) { - throw new Error(`${SCROLL_JANK_V3_TRACK_KIND} track is not registered.`); - } - - const slices = await getSlicesFromTrack(engine, track, { - filters: [`event_latency_id=${id}`], - }); - return slices; -} - -export type EventLatencySlice = BasicSlice; -export async function getEventLatencySlice( - engine: Engine, - id: number, -): Promise { - const track = ScrollJankPluginState.getInstance().getTrack( - CHROME_EVENT_LATENCY_TRACK_KIND, - ); - if (track == undefined) { - throw new Error( - `${CHROME_EVENT_LATENCY_TRACK_KIND} track is not registered.`, - ); - } - - const slices = await getSlicesFromTrack(engine, track, { - filters: [`id=${id}`], - }); - return slices[0]; -} - -export async function getEventLatencyDescendantSlice( - engine: Engine, - id: number, - descendant: string | undefined, -): Promise { - const query = await engine.query(` - SELECT - id as sliceId, - ts, - dur as dur - FROM descendant_slice(${id}) - WHERE name='${descendant}'`); - const it = query.iter({ - sliceId: NUM, - ts: LONG, - dur: LONG, - }); - - const result: EventLatencySlice[] = []; - - for (; it.valid(); it.next()) { - result.push({ - sliceId: it.sliceId as SliceSqlId, - ts: Time.fromRaw(it.ts), - dur: it.dur, - }); - } - - const eventLatencyTrack = ScrollJankPluginState.getInstance().getTrack( - CHROME_EVENT_LATENCY_TRACK_KIND, - ); - if (eventLatencyTrack == undefined) { - throw new Error( - `${CHROME_EVENT_LATENCY_TRACK_KIND} track is not registered.`, - ); - } - - if (result.length > 1) { - throw new Error(` - Slice table and track view ${eventLatencyTrack.sqlTableName} has more than one descendant of slice id ${id} with name ${descendant}`); - } - if (result.length === 0) { - return undefined; - } - return result[0]; -} - -interface BasicScrollJankSliceRefAttrs { - id: number; - ts: time; - dur: duration; - name: string; - kind: string; -} - -export class ScrollJankSliceRef - implements m.ClassComponent -{ - view(vnode: m.Vnode) { - return m( - Anchor, - { - icon: Icons.UpdateSelection, - onclick: () => { - const track = ScrollJankPluginState.getInstance().getTrack( - vnode.attrs.kind, - ); - if (track == undefined) { - throw new Error(`${vnode.attrs.kind} track is not registered.`); - } - - const trackUri = track.key; - globals.selectionManager.selectGenericSlice({ - id: vnode.attrs.id, - sqlTableName: track.sqlTableName, - start: vnode.attrs.ts, - duration: vnode.attrs.dur, - trackUri, - detailsPanelConfig: track.detailsPanelConfig, - }); - - scrollTo({ - track: {uri: trackUri, expandGroup: true}, - time: {start: vnode.attrs.ts}, - }); - }, - }, - vnode.attrs.name, - ); - } -} - -export function getSliceForTrack( - state: BasicSlice, - trackKind: string, - name: string, -): m.Child { - return m(ScrollJankSliceRef, { - id: state.sliceId, - ts: state.ts, - dur: state.dur, - name: name, - kind: trackKind, - }); -} diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts index 48fd9886bc..280fd1882e 100644 --- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts +++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts @@ -16,8 +16,6 @@ import m from 'mithril'; import {duration, Time, time} from '../../base/time'; import {exists} from '../../base/utils'; import {raf} from '../../core/raf_scheduler'; -import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab'; -import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab'; import {getSlice, SliceDetails} from '../../trace_processor/sql_utils/slice'; import {asSliceSqlId} from '../../trace_processor/sql_utils/core_types'; import {DurationWidget} from '../../frontend/widgets/duration'; @@ -30,13 +28,9 @@ import {Section} from '../../widgets/section'; import {SqlRef} from '../../widgets/sql_ref'; import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph'; import {dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree'; -import { - EventLatencySlice, - getEventLatencyDescendantSlice, - getEventLatencySlice, - getSliceForTrack, -} from './scroll_jank_slice'; -import {CHROME_EVENT_LATENCY_TRACK_KIND} from '../../public/track_kinds'; +import {EVENT_LATENCY_TRACK_URI, renderSliceRef} from './selection_utils'; +import {TrackEventDetailsPanel} from '../../public/details_panel'; +import {Trace} from '../../public/trace'; interface Data { name: string; @@ -64,10 +58,8 @@ async function getSliceDetails( return getSlice(engine, asSliceSqlId(id)); } -export class ScrollJankV3DetailsPanel extends BottomTab { - static readonly kind = 'org.perfetto.ScrollJankV3DetailsPanel'; - data: Data | undefined; - loaded = false; +export class ScrollJankV3DetailsPanel implements TrackEventDetailsPanel { + private data?: Data; // // Linking to associated slices @@ -80,29 +72,34 @@ export class ScrollJankV3DetailsPanel extends BottomTab, - ): ScrollJankV3DetailsPanel { - return new ScrollJankV3DetailsPanel(args); - } - - constructor(args: NewBottomTabArgs) { - super(args); - this.loadData(); - } - - private async loadData() { - const queryResult = await this.engine.query(` + private subcauseSliceDetails?: { + id: number; + ts: time; + dur: duration; + }; + + constructor( + private readonly trace: Trace, + private readonly id: number, + ) {} + + async load() { + const queryResult = await this.trace.engine.query(` SELECT IIF( cause_of_jank IS NOT NULL, @@ -117,7 +114,7 @@ export class ScrollJankV3DetailsPanel extends BottomTab { - await super.onDestroy(); - ScrollJankPluginState.getInstance().unregisterTrack( - SCROLL_JANK_V3_TRACK_KIND, - ); - } - rowToSlice(row: NamedRow): Slice { const slice = super.rowToSlice(row); @@ -92,20 +59,4 @@ export class ScrollJankV3Track extends CustomSqlTableSliceTrack { return {...slice, colorScheme: getColorForSlice(stage)}; } } - - onUpdatedSlices(slices: Slice[]) { - for (const slice of slices) { - const currentSelection = globals.selectionManager.legacySelection; - const isSelected = - currentSelection && - currentSelection.kind === 'GENERIC_SLICE' && - currentSelection.id !== undefined && - currentSelection.id === slice.id; - - const highlighted = globals.state.highlightedSliceId === slice.id; - const hasFocus = highlighted || isSelected; - slice.isHighlighted = !!hasFocus; - } - super.onUpdatedSlices(slices); - } } diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts index 1ee7d2c2d2..fcd20fdc10 100644 --- a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts +++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts @@ -12,51 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {NewTrackArgs} from '../../frontend/track'; -import {CHROME_TOPLEVEL_SCROLLS_KIND} from '../../public/track_kinds'; import { - CustomSqlDetailsPanelConfig, CustomSqlTableDefConfig, CustomSqlTableSliceTrack, } from '../../frontend/tracks/custom_sql_table_slice_track'; -import {ScrollJankPluginState} from './common'; -import {ScrollDetailsPanel} from './scroll_details_panel'; export class TopLevelScrollTrack extends CustomSqlTableSliceTrack { - public static kind = CHROME_TOPLEVEL_SCROLLS_KIND; - getSqlDataSource(): CustomSqlTableDefConfig { return { columns: [`printf("Scroll %s", CAST(id AS STRING)) AS name`, '*'], sqlTableName: 'chrome_scrolls', }; } - - getDetailsPanel(): CustomSqlDetailsPanelConfig { - return { - kind: ScrollDetailsPanel.kind, - config: { - sqlTableName: this.tableName, - title: 'Chrome Top Level Scrolls', - }, - }; - } - - constructor(args: NewTrackArgs) { - super(args); - - ScrollJankPluginState.getInstance().registerTrack({ - kind: TopLevelScrollTrack.kind, - trackUri: this.uri, - tableName: this.tableName, - detailsPanelConfig: this.getDetailsPanel(), - }); - } - - async onDestroy(): Promise { - await super.onDestroy(); - ScrollJankPluginState.getInstance().unregisterTrack( - TopLevelScrollTrack.kind, - ); - } } diff --git a/ui/src/core_plugins/chrome_scroll_jank/selection_utils.ts b/ui/src/core_plugins/chrome_scroll_jank/selection_utils.ts new file mode 100644 index 0000000000..4b79e05b26 --- /dev/null +++ b/ui/src/core_plugins/chrome_scroll_jank/selection_utils.ts @@ -0,0 +1,42 @@ +// 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 m from 'mithril'; +import {Anchor} from '../../widgets/anchor'; +import {Icons} from '../../base/semantic_icons'; +import {Trace} from '../../public/trace'; + +export const SCROLLS_TRACK_URI = 'perfetto.ChromeScrollJank#toplevelScrolls'; +export const EVENT_LATENCY_TRACK_URI = 'perfetto.ChromeScrollJank#eventLatency'; +export const JANKS_TRACK_URI = 'perfetto.ChromeScrollJank#scrollJankV3'; + +export function renderSliceRef(args: { + trace: Trace; + id: number; + trackUri: string; + title: m.Children; +}) { + return m( + Anchor, + { + icon: Icons.UpdateSelection, + onclick: () => { + args.trace.selection.selectTrackEvent(args.trackUri, args.id, { + scrollToSelection: true, + }); + }, + }, + args.title, + ); +} diff --git a/ui/src/core_plugins/chrome_tasks/details.ts b/ui/src/core_plugins/chrome_tasks/details.ts index 56acae154e..ac96447b61 100644 --- a/ui/src/core_plugins/chrome_tasks/details.ts +++ b/ui/src/core_plugins/chrome_tasks/details.ts @@ -13,25 +13,21 @@ // limitations under the License. import m from 'mithril'; -import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab'; -import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab'; import { Details, DetailsSchema, } from '../../frontend/widgets/sql/details/details'; import {DetailsShell} from '../../widgets/details_shell'; import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout'; +import {TrackEventDetailsPanel} from '../../public/details_panel'; +import {Trace} from '../../public/trace'; import d = DetailsSchema; -export class ChromeTasksDetailsTab extends BottomTab { - static readonly kind = 'org.chromium.ChromeTasks.TaskDetailsTab'; +export class ChromeTasksDetailsPanel implements TrackEventDetailsPanel { + private readonly data: Details; - private data: Details; - - constructor(args: NewBottomTabArgs) { - super(args); - - this.data = new Details(this.trace, 'chrome_tasks', this.config.id, { + constructor(trace: Trace, eventId: number) { + this.data = new Details(trace, 'chrome_tasks', eventId, { 'Task name': 'name', 'Start time': d.Timestamp('ts'), 'Duration': d.Interval('ts', 'dur'), @@ -41,21 +37,13 @@ export class ChromeTasksDetailsTab extends BottomTab { + return new ChromeTasksDetailsPanel(ctx, sel.eventId); + }, }); const track = new TrackNode({uri, title}); group.addChildInOrder(track); ctx.workspace.addChildInOrder(group); } - - ctx.tabs.registerDetailsPanel( - new BottomTabToSCSAdapter({ - tabFactory: (selection) => { - if ( - selection.kind === 'GENERIC_SLICE' && - selection.detailsPanelConfig.kind === ChromeTasksDetailsTab.kind - ) { - const config = selection.detailsPanelConfig.config; - return new ChromeTasksDetailsTab({ - config: config as GenericSliceDetailsTabConfig, - trace: ctx, - uuid: uuidv4(), - }); - } - return undefined; - }, - }), - ); } } diff --git a/ui/src/core_plugins/chrome_tasks/track.ts b/ui/src/core_plugins/chrome_tasks/track.ts index e96735c5f7..24203ea79e 100644 --- a/ui/src/core_plugins/chrome_tasks/track.ts +++ b/ui/src/core_plugins/chrome_tasks/track.ts @@ -14,11 +14,9 @@ import {Utid} from '../../trace_processor/sql_utils/core_types'; import { - CustomSqlDetailsPanelConfig, CustomSqlTableDefConfig, CustomSqlTableSliceTrack, } from '../../frontend/tracks/custom_sql_table_slice_track'; -import {ChromeTasksDetailsTab} from './details'; import {Trace} from '../../public/trace'; export class ChromeTasksThreadTrack extends CustomSqlTableSliceTrack { @@ -37,14 +35,4 @@ export class ChromeTasksThreadTrack extends CustomSqlTableSliceTrack { whereClause: `utid = ${this.utid}`, }; } - - getDetailsPanel(): CustomSqlDetailsPanelConfig { - return { - kind: ChromeTasksDetailsTab.kind, - config: { - sqlTableName: 'chrome_tasks', - title: 'Chrome Tasks', - }, - }; - } } diff --git a/ui/src/core_plugins/debug/index.ts b/ui/src/core_plugins/debug/index.ts index 3a439770f6..3ff5fcb75a 100644 --- a/ui/src/core_plugins/debug/index.ts +++ b/ui/src/core_plugins/debug/index.ts @@ -12,16 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {uuidv4} from '../../base/uuid'; import { addDebugCounterTrack, addDebugSliceTrack, } from '../../public/lib/debug_tracks/debug_tracks'; -import {BottomTabToSCSAdapter} from '../../public/utils'; import {Trace} from '../../public/trace'; import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin'; -import {DebugSliceDetailsTab} from '../../public/lib/debug_tracks/details_tab'; -import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab'; import {Optional, exists} from '../../base/utils'; class DebugTracksPlugin implements PerfettoPlugin { @@ -65,28 +61,6 @@ class DebugTracksPlugin implements PerfettoPlugin { } }, }); - - // TODO(stevegolton): While debug tracks are in their current state, we rely - // on this plugin to provide the details panel for them. In the future, this - // details panel will become part of the debug track's definition. - ctx.tabs.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, - trace: ctx, - uuid: uuidv4(), - }); - } - return undefined; - }, - }), - ); } } diff --git a/ui/src/core_plugins/screenshots/index.ts b/ui/src/core_plugins/screenshots/index.ts index b7e41e2281..5c8ee657b8 100644 --- a/ui/src/core_plugins/screenshots/index.ts +++ b/ui/src/core_plugins/screenshots/index.ts @@ -12,14 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {uuidv4} from '../../base/uuid'; -import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab'; import {TrackNode} from '../../public/workspace'; -import {BottomTabToSCSAdapter} from '../../public/utils'; import {NUM} from '../../trace_processor/query_result'; import {Trace} from '../../public/trace'; import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin'; -import {ScreenshotTab} from './screenshot_panel'; +import {ScreenshotDetailsPanel} from './screenshot_panel'; import {ScreenshotsTrack} from './screenshots_track'; class ScreenshotsPlugin implements PerfettoPlugin { @@ -45,28 +42,10 @@ class ScreenshotsPlugin implements PerfettoPlugin { tags: { kind: ScreenshotsTrack.kind, }, + detailsPanel: () => new ScreenshotDetailsPanel(ctx.engine), }); const trackNode = new TrackNode({uri, title, sortOrder: -60}); ctx.workspace.addChildInOrder(trackNode); - - ctx.tabs.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, - trace: ctx, - uuid: uuidv4(), - }); - } - return undefined; - }, - }), - ); } } } diff --git a/ui/src/core_plugins/screenshots/screenshot_panel.ts b/ui/src/core_plugins/screenshots/screenshot_panel.ts index a1d8cd44e2..a5424af1b1 100644 --- a/ui/src/core_plugins/screenshots/screenshot_panel.ts +++ b/ui/src/core_plugins/screenshots/screenshot_panel.ts @@ -15,44 +15,25 @@ import m from 'mithril'; import {assertTrue} from '../../base/logging'; import {exists} from '../../base/utils'; -import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab'; -import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab'; import {getSlice, SliceDetails} from '../../trace_processor/sql_utils/slice'; import {asSliceSqlId} from '../../trace_processor/sql_utils/core_types'; import {Engine} from '../../trace_processor/engine'; +import {TrackEventDetailsPanel} from '../../public/details_panel'; +import {TrackEventSelection} from '../../public/selection'; -async function getSliceDetails( - engine: Engine, - id: number, -): Promise { - return getSlice(engine, asSliceSqlId(id)); -} - -export class ScreenshotTab extends BottomTab { - static readonly kind = 'dev.perfetto.ScreenshotDetailsPanel'; - +export class ScreenshotDetailsPanel implements TrackEventDetailsPanel { private sliceDetails?: SliceDetails; - static create( - args: NewBottomTabArgs, - ): ScreenshotTab { - return new ScreenshotTab(args); - } + constructor(private readonly engine: Engine) {} - constructor(args: NewBottomTabArgs) { - super(args); - getSliceDetails(this.engine, this.config.id).then( - (sliceDetails) => (this.sliceDetails = sliceDetails), + async load(selection: TrackEventSelection) { + this.sliceDetails = await getSlice( + this.engine, + asSliceSqlId(selection.eventId), ); } - renderTabCanvas() {} - - getTitle() { - return this.config.title; - } - - viewTab() { + render() { if ( !exists(this.sliceDetails) || !exists(this.sliceDetails.args) || diff --git a/ui/src/core_plugins/screenshots/screenshots_track.ts b/ui/src/core_plugins/screenshots/screenshots_track.ts index d88010e9c2..e9decdc325 100644 --- a/ui/src/core_plugins/screenshots/screenshots_track.ts +++ b/ui/src/core_plugins/screenshots/screenshots_track.ts @@ -13,11 +13,9 @@ // limitations under the License. import { - CustomSqlDetailsPanelConfig, CustomSqlTableDefConfig, CustomSqlTableSliceTrack, } from '../../frontend/tracks/custom_sql_table_slice_track'; -import {ScreenshotTab} from './screenshot_panel'; export class ScreenshotsTrack extends CustomSqlTableSliceTrack { static readonly kind = 'dev.perfetto.ScreenshotsTrack'; @@ -28,14 +26,4 @@ export class ScreenshotsTrack extends CustomSqlTableSliceTrack { columns: ['*'], }; } - - getDetailsPanel(): CustomSqlDetailsPanelConfig { - return { - kind: ScreenshotTab.kind, - config: { - sqlTableName: this.tableName, - title: 'Screenshots', - }, - }; - } } diff --git a/ui/src/core_plugins/test_plugin/index.ts b/ui/src/core_plugins/test_plugin/index.ts index 2b7ccf12b7..0979836dd0 100644 --- a/ui/src/core_plugins/test_plugin/index.ts +++ b/ui/src/core_plugins/test_plugin/index.ts @@ -19,6 +19,7 @@ import { SimpleSliceTrackConfig, } from '../../frontend/simple_slice_track'; import {TrackNode} from '../../public/workspace'; +import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab'; class Plugin implements PerfettoPlugin { async onTraceLoad(ctx: Trace): Promise { @@ -49,10 +50,13 @@ class Plugin implements PerfettoPlugin { const title = 'Test Track'; const uri = `/test_track`; + const track = new SimpleSliceTrack(ctx, {trackUri: uri}, config); ctx.tracks.registerTrack({ uri, title, - track: new SimpleSliceTrack(ctx, {trackUri: uri}, config), + track, + detailsPanel: ({eventId}) => + new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId), }); this.addNestedTracks(ctx, uri); diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts index 79aae89169..18005dfe75 100644 --- a/ui/src/frontend/base_slice_track.ts +++ b/ui/src/frontend/base_slice_track.ts @@ -21,7 +21,7 @@ import {drawIncompleteSlice, drawTrackHoverTooltip} from '../base/canvas_utils'; import {cropText} from '../base/string_utils'; import {colorCompare} from '../public/color'; import {UNEXPECTED_PINK} from '../core/colorizer'; -import {LegacySelection, TrackEventDetails} from '../public/selection'; +import {TrackEventDetails} from '../public/selection'; import {featureFlags} from '../core/feature_flags'; import {raf} from '../core/raf_scheduler'; import {Track} from '../public/track'; @@ -277,10 +277,6 @@ export abstract class BaseSliceTrack< } } - protected isSelectionHandled(_selection: LegacySelection): boolean { - return false; - } - private getTitleFont(): string { const size = this.sliceLayout.titleSizePx ?? 12; return `${size}px Roboto Condensed`; @@ -397,21 +393,11 @@ export abstract class BaseSliceTrack< visibleWindow.end.toTime('ceil'), ); - let selectedId: number | undefined = undefined; const selection = globals.selectionManager.selection; - switch (selection.kind) { - case 'track_event': - if (selection.trackUri === this.uri) { - selectedId = selection.eventId; - } - break; - case 'legacy': - const legacySelection = selection.legacySelection; - if (this.isSelectionHandled(legacySelection)) { - selectedId = (legacySelection as {id: number}).id; - } - break; - } + const selectedId = + selection.kind === 'track_event' && selection.trackUri === this.uri + ? selection.eventId + : undefined; if (selectedId === undefined) { this.selectedSlice = undefined; diff --git a/ui/src/frontend/generic_slice_details_tab.ts b/ui/src/frontend/generic_slice_details_tab.ts index 289e21701c..2389134181 100644 --- a/ui/src/frontend/generic_slice_details_tab.ts +++ b/ui/src/frontend/generic_slice_details_tab.ts @@ -13,8 +13,7 @@ // limitations under the License. import m from 'mithril'; -import {GenericSliceDetailsTabConfig} from '../public/details_panel'; -import {raf} from '../core/raf_scheduler'; +import {Columns, TrackEventDetailsPanel} from '../public/details_panel'; import {ColumnType} from '../trace_processor/query_result'; import {sqlValueToReadableString} from '../trace_processor/sql_utils'; import {DetailsShell} from '../widgets/details_shell'; @@ -22,7 +21,7 @@ import {GridLayout} from '../widgets/grid_layout'; import {Section} from '../widgets/section'; import {SqlRef} from '../widgets/sql_ref'; import {dictToTree, Tree, TreeNode} from '../widgets/tree'; -import {BottomTab, NewBottomTabArgs} from '../public/lib/bottom_tab'; +import {Trace} from '../public/trace'; export { ColumnConfig, @@ -34,41 +33,36 @@ export { // A details tab, which fetches slice-like object from a given SQL table by id // and renders it according to the provided config, specifying which columns // need to be rendered and how. -export class GenericSliceDetailsTab extends BottomTab { - static readonly kind = 'dev.perfetto.GenericSliceDetailsTab'; +export class GenericSliceDetailsTab implements TrackEventDetailsPanel { + private data?: {[key: string]: ColumnType}; - data: {[key: string]: ColumnType} | undefined; + constructor( + private readonly trace: Trace, + private readonly sqlTableName: string, + private readonly id: number, + private readonly title: string, + private readonly columns?: Columns, + ) {} - static create( - args: NewBottomTabArgs, - ): GenericSliceDetailsTab { - return new GenericSliceDetailsTab(args); - } - - constructor(args: NewBottomTabArgs) { - super(args); + async load() { + const result = await this.trace.engine.query( + `select * from ${this.sqlTableName} where id = ${this.id}`, + ); - this.engine - .query( - `select * from ${this.config.sqlTableName} where id = ${this.config.id}`, - ) - .then((queryResult) => { - this.data = queryResult.firstRow({}); - raf.scheduleFullRedraw(); - }); + this.data = result.firstRow({}); } - viewTab() { - if (this.data === undefined) { + render() { + if (!this.data) { return m('h2', 'Loading'); } const args: {[key: string]: m.Child} = {}; - if (this.config.columns !== undefined) { - for (const key of Object.keys(this.config.columns)) { + if (this.columns !== undefined) { + for (const key of Object.keys(this.columns)) { let argKey = key; - if (this.config.columns[key].displayName !== undefined) { - argKey = this.config.columns[key].displayName!; + if (this.columns[key].displayName !== undefined) { + argKey = this.columns[key].displayName!; } args[argKey] = sqlValueToReadableString(this.data[key]); } @@ -83,7 +77,7 @@ export class GenericSliceDetailsTab extends BottomTab; - // Override by subclasses. - abstract getDetailsPanel( - args: OnSliceClickArgs, - ): CustomSqlDetailsPanelConfig; - getSqlImports(): CustomSqlImportConfig { return { modules: [] as string[], @@ -109,32 +101,6 @@ export abstract class CustomSqlTableSliceTrack extends NamedSliceTrack< return `SELECT * FROM ${this.tableName}`; } - isSelectionHandled(selection: LegacySelection) { - if (selection.kind !== 'GENERIC_SLICE') { - return false; - } - return selection.trackUri === this.uri; - } - - onSliceClick(args: OnSliceClickArgs) { - if (this.getDetailsPanel(args) === undefined) { - return; - } - - const detailsPanelConfig = this.getDetailsPanel(args); - globals.selectionManager.selectGenericSlice({ - id: args.slice.id, - sqlTableName: this.tableName, - start: args.slice.ts, - duration: args.slice.dur, - trackUri: this.uri, - detailsPanelConfig: { - kind: detailsPanelConfig.kind, - config: detailsPanelConfig.config, - }, - }); - } - async loadImports() { for (const importModule of this.getSqlImports().modules) { await this.engine.query(`INCLUDE PERFETTO MODULE ${importModule};`); diff --git a/ui/src/plugins/com.android.InputEvents/index.ts b/ui/src/plugins/com.android.InputEvents/index.ts index 7a529d3720..9116ad3ce9 100644 --- a/ui/src/plugins/com.android.InputEvents/index.ts +++ b/ui/src/plugins/com.android.InputEvents/index.ts @@ -21,6 +21,7 @@ import { } from '../../frontend/simple_slice_track'; import {TrackNode} from '../../public/workspace'; import {getOrCreateUserInteractionGroup} from '../../public/standard_groups'; +import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab'; class InputEvents implements PerfettoPlugin { private readonly SQL_SOURCE = ` @@ -33,12 +34,12 @@ class InputEvents implements PerfettoPlugin { `; async onTraceLoad(ctx: Trace): Promise { - const cnt = await(ctx.engine.query(` + const cnt = await ctx.engine.query(` SELECT count(*) as cnt FROM slice WHERE name GLOB 'UnwantedInteractionBlocker::notifyMotion*' - `)); + `); if (cnt.firstRow({cnt: LONG}).cnt == 0n) { return; } @@ -51,13 +52,16 @@ class InputEvents implements PerfettoPlugin { columns: {ts: 'ts', dur: 'dur', name: 'name'}, argColumns: [], }; - await ctx.engine.query("INCLUDE PERFETTO MODULE android.input;"); + await ctx.engine.query('INCLUDE PERFETTO MODULE android.input;'); const uri = 'com.android.InputEvents#InputEventsTrack'; const title = 'Input Events'; + const track = new SimpleSliceTrack(ctx, {trackUri: uri}, config); ctx.tracks.registerTrack({ uri, title: title, - track: new SimpleSliceTrack(ctx, {trackUri: uri}, config) + track, + detailsPanel: ({eventId}) => + new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId), }); const node = new TrackNode({uri, title}); const group = getOrCreateUserInteractionGroup(ctx.workspace); diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts index 6a2dd1e60e..0968acd708 100644 --- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts +++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts @@ -25,6 +25,7 @@ import { SimpleCounterTrackConfig, } from '../../frontend/simple_counter_track'; import {TrackNode} from '../../public/workspace'; +import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab'; interface ContainedTrace { uuid: string; @@ -1184,13 +1185,16 @@ class AndroidLongBatteryTracing implements PerfettoPlugin { }; const uri = `/long_battery_tracing_${name}`; + const track = new SimpleSliceTrack(ctx, {trackUri: uri}, config); ctx.tracks.registerTrack({ uri, title: name, - track: new SimpleSliceTrack(ctx, {trackUri: uri}, config), + track, + detailsPanel: ({eventId}) => + new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId), }); - const track = new TrackNode({uri, title: name}); - this.addTrack(ctx, track, groupName); + const trackNode = new TrackNode({uri, title: name}); + this.addTrack(ctx, trackNode, groupName); } addCounterTrack( diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts index e734528656..a53dd2a249 100644 --- a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts +++ b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts @@ -20,6 +20,7 @@ import { SimpleSliceTrackConfig, } from '../../frontend/simple_slice_track'; import {TrackNode} from '../../public/workspace'; +import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab'; class AndroidStartup implements PerfettoPlugin { async onTraceLoad(ctx: Trace): Promise { const e = ctx.engine; @@ -43,13 +44,16 @@ class AndroidStartup implements PerfettoPlugin { }; const uri = `/android_startups`; const title = 'Android App Startups'; + const track = new SimpleSliceTrack(ctx, {trackUri: uri}, config); ctx.tracks.registerTrack({ uri, title: 'Android App Startups', - track: new SimpleSliceTrack(ctx, {trackUri: uri}, config), + track, + detailsPanel: ({eventId}) => + new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId), }); - const track = new TrackNode({title, uri}); - ctx.workspace.addChildInOrder(track); + const trackNode = new TrackNode({title, uri}); + ctx.workspace.addChildInOrder(trackNode); } } diff --git a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts index 4199c448f3..fc660e216b 100644 --- a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts +++ b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts @@ -17,6 +17,7 @@ import {Trace} from '../../public/trace'; import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin'; import {SimpleSliceTrack} from '../../frontend/simple_slice_track'; import {TrackNode} from '../../public/workspace'; +import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab'; class TraceMetadata implements PerfettoPlugin { async onTraceLoad(ctx: Trace): Promise { const res = await ctx.engine.query(` @@ -28,27 +29,30 @@ class TraceMetadata implements PerfettoPlugin { } const uri = `/clock_snapshots`; const title = 'Clock Snapshots'; + const track = new SimpleSliceTrack( + ctx, + {trackUri: uri}, + { + data: { + sqlSource: ` + select ts, 0 as dur, 'Snapshot' as name + from clock_snapshot + `, + columns: ['ts', 'dur', 'name'], + }, + columns: {ts: 'ts', dur: 'dur', name: 'name'}, + argColumns: [], + }, + ); ctx.tracks.registerTrack({ uri, title, - track: new SimpleSliceTrack( - ctx, - {trackUri: uri}, - { - data: { - sqlSource: ` - select ts, 0 as dur, 'Snapshot' as name - from clock_snapshot - `, - columns: ['ts', 'dur', 'name'], - }, - columns: {ts: 'ts', dur: 'dur', name: 'name'}, - argColumns: [], - }, - ), + track, + detailsPanel: ({eventId}) => + new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId), }); - const track = new TrackNode({uri, title}); - ctx.workspace.addChildInOrder(track); + const trackNode = new TrackNode({uri, title}); + ctx.workspace.addChildInOrder(trackNode); } } diff --git a/ui/src/public/lib/debug_tracks/debug_tracks.ts b/ui/src/public/lib/debug_tracks/debug_tracks.ts index f85290f2f8..387b8f2b5c 100644 --- a/ui/src/public/lib/debug_tracks/debug_tracks.ts +++ b/ui/src/public/lib/debug_tracks/debug_tracks.ts @@ -19,9 +19,10 @@ import { sqlValueToReadableString, } from '../../../trace_processor/sql_utils'; import {DebugCounterTrack} from './counter_track'; -import {ARG_PREFIX} from './details_tab'; +import {ARG_PREFIX, DebugSliceDetailsPanel} from './details_tab'; import {TrackNode} from '../../workspace'; import {Trace} from '../../trace'; +import {TrackEventSelection} from '../../selection'; let trackCounter = 0; // For reproducible ids. @@ -123,6 +124,9 @@ export async function addDebugSliceTrack( uri, title: trackName, track: new DebugSliceTrack(trace, {trackUri: uri}, tableName), + detailsPanel: (sel: TrackEventSelection) => { + return new DebugSliceDetailsPanel(trace, tableName, sel.eventId); + }, }); // Create the actions to add this track to the tracklist diff --git a/ui/src/public/lib/debug_tracks/details_tab.ts b/ui/src/public/lib/debug_tracks/details_tab.ts index 615f8420b2..fe7c61efe6 100644 --- a/ui/src/public/lib/debug_tracks/details_tab.ts +++ b/ui/src/public/lib/debug_tracks/details_tab.ts @@ -14,8 +14,6 @@ import m from 'mithril'; import {duration, Time, time} from '../../../base/time'; -import {BottomTab, NewBottomTabArgs} from '../bottom_tab'; -import {GenericSliceDetailsTabConfig} from '../../../frontend/generic_slice_details_tab'; import {hasArgs, renderArguments} from '../../../frontend/slice_args'; import {getSlice, SliceDetails} from '../../../trace_processor/sql_utils/slice'; import { @@ -49,6 +47,8 @@ import {threadStateRef} from '../../../frontend/widgets/thread_state'; import {getThreadName} from '../../../trace_processor/sql_utils/thread'; import {getProcessName} from '../../../trace_processor/sql_utils/process'; import {sliceRef} from '../../../frontend/widgets/slice'; +import {TrackEventDetailsPanel} from '../../details_panel'; +import {Trace} from '../../trace'; export const ARG_PREFIX = 'arg_'; @@ -78,10 +78,8 @@ function renderTreeContents(dict: {[key: string]: m.Child}): m.Child[] { return children; } -export class DebugSliceDetailsTab extends BottomTab { - static readonly kind = 'dev.perfetto.DebugSliceDetailsTab'; - - data?: { +export class DebugSliceDetailsPanel implements TrackEventDetailsPanel { + private data?: { name: string; ts: time; dur: duration; @@ -91,14 +89,14 @@ export class DebugSliceDetailsTab extends BottomTab, - ): DebugSliceDetailsTab { - return new DebugSliceDetailsTab(args); - } + constructor( + private readonly trace: Trace, + private readonly tableName: string, + private readonly eventId: number, + ) {} private async maybeLoadThreadState( id: number | undefined, @@ -110,7 +108,7 @@ export class DebugSliceDetailsTab extends BottomTab) { - super(args); - this.loadData(); - } - - viewTab() { + render() { if (this.data === undefined) { return m('h2', 'Loading'); } @@ -250,7 +243,7 @@ export class DebugSliceDetailsTab extends BottomTab { diff --git a/ui/src/public/selection.ts b/ui/src/public/selection.ts index 49659109c5..69a1a87395 100644 --- a/ui/src/public/selection.ts +++ b/ui/src/public/selection.ts @@ -16,12 +16,10 @@ import {time, duration, TimeSpan} from '../base/time'; import {Optional} from '../base/utils'; import {Engine} from '../trace_processor/engine'; import {ColumnDef, Sorting, ThreadStateExtra} from './aggregation'; -import {GenericSliceDetailsTabConfigBase} from './details_panel'; import {TrackDescriptor} from './track'; export interface SelectionManager { readonly selection: Selection; - readonly legacySelection: LegacySelection | null; findTimeRangeOfSelection(): Optional; clear(): void; @@ -48,14 +46,6 @@ export interface SelectionManager { */ selectSqlEvent(sqlTableName: string, id: number, opts?: SelectionOpts): void; - /** - * Select a legacy selection. - * - * @param selection - The legacy selection to select. - * @param opts - Additional options. - */ - selectLegacy(selection: LegacySelection, opts?: SelectionOpts): void; - /** * Create an area selection for the purposes of aggregation. * @@ -67,20 +57,6 @@ export interface SelectionManager { scrollToCurrentSelection(): void; registerAreaSelectionAggreagtor(aggr: AreaSelectionAggregator): void; - // TODO(primiano): I don't undertsand what this generic slice is, but now - // is exposed to plugins. For now i'm just carrying it forward. - selectGenericSlice(args: { - id: number; - sqlTableName: string; - start: time; - duration: duration; - trackUri: string; - detailsPanelConfig: { - kind: string; - config: GenericSliceDetailsTabConfigBase; - }; - }): void; - /** * Register a new SQL selection resolver. * @@ -108,8 +84,7 @@ export type Selection = | AreaSelection | NoteSelection | UnionSelection - | EmptySelection - | LegacySelectionWrapper; + | EmptySelection; /** Defines how changes to selection affect the rest of the UI state */ export interface SelectionOpts { @@ -118,32 +93,6 @@ export interface SelectionOpts { scrollToSelection?: boolean; // Default: false. } -// LEGACY Selection types: - -export interface LegacySelectionWrapper { - readonly kind: 'legacy'; - readonly legacySelection: LegacySelection; -} - -export type LegacySelection = GenericSliceSelection & { - trackUri?: string; -}; - -export interface GenericSliceSelection { - readonly kind: 'GENERIC_SLICE'; - readonly id: number; - readonly sqlTableName: string; - readonly start: time; - readonly duration: duration; - // NOTE: this config can be expanded for multiple details panel types. - readonly detailsPanelConfig: { - readonly kind: string; - readonly config: GenericSliceDetailsTabConfigBase; - }; -} - -// New Selection types: - export interface TrackEventSelection extends TrackEventDetails { readonly kind: 'track_event'; readonly trackUri: string; @@ -165,6 +114,7 @@ export interface TrackEventDetails { readonly utid?: number; readonly tableName?: string; readonly profileType?: ProfileType; + readonly interactionType?: string; } export interface Area { diff --git a/ui/src/public/track.ts b/ui/src/public/track.ts index 5f32e6bed8..d12faed9d3 100644 --- a/ui/src/public/track.ts +++ b/ui/src/public/track.ts @@ -106,7 +106,7 @@ export interface TrackDescriptor { // Optional: A factory that returns a details panel object. This is called // each time the selection is changed (and the selection is relevant to this // track). - readonly detailsPanel?: (id: TrackEventSelection) => TrackEventDetailsPanel; + readonly detailsPanel?: (sel: TrackEventSelection) => TrackEventDetailsPanel; } /** diff --git a/ui/src/public/track_kinds.ts b/ui/src/public/track_kinds.ts index 05671c967c..e5df1aea71 100644 --- a/ui/src/public/track_kinds.ts +++ b/ui/src/public/track_kinds.ts @@ -27,12 +27,4 @@ export const COUNTER_TRACK_KIND = 'CounterTrack'; export const CPUSS_ESTIMATE_TRACK_KIND = 'CpuSubsystemEstimateTrack'; export const CPU_PROFILE_TRACK_KIND = 'CpuProfileTrack'; export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack'; -export const CHROME_TOPLEVEL_SCROLLS_KIND = - 'org.chromium.TopLevelScrolls.scrolls'; -export const CHROME_EVENT_LATENCY_TRACK_KIND = - 'org.chromium.ScrollJank.event_latencies'; -export const SCROLL_JANK_V3_TRACK_KIND = - 'org.chromium.ScrollJank.scroll_jank_v3_track'; -export const CHROME_SCROLL_JANK_TRACK_KIND = - 'org.chromium.ScrollJank.BrowserUIThreadLongTasks'; export const ANDROID_LOGS_TRACK_KIND = 'AndroidLogTrack'; diff --git a/ui/src/public/utils.ts b/ui/src/public/utils.ts index 6e6fe02e65..208961a3a6 100644 --- a/ui/src/public/utils.ts +++ b/ui/src/public/utils.ts @@ -13,11 +13,9 @@ // limitations under the License. import m from 'mithril'; -import {LegacySelection, Selection} from '../public/selection'; import {BottomTab} from './lib/bottom_tab'; import {Tab} from './tab'; import {exists} from '../base/utils'; -import {DetailsPanel} from './details_panel'; import {Trace} from './trace'; import {TimeSpan} from '../base/time'; @@ -98,10 +96,6 @@ export function getTrackName( return 'Unknown'; } -export interface BottomTabAdapterAttrs { - tabFactory: (sel: LegacySelection) => BottomTab | undefined; -} - /** * This adapter wraps a BottomTab, converting it into a the new "current * selection" API. @@ -130,34 +124,6 @@ export interface BottomTabAdapterAttrs { }, }) */ -export class BottomTabToSCSAdapter implements DetailsPanel { - private oldSelection?: Selection; - private bottomTab?: BottomTab; - private attrs: BottomTabAdapterAttrs; - - constructor(attrs: BottomTabAdapterAttrs) { - this.attrs = attrs; - } - - render(selection: Selection): m.Children { - // Detect selection changes, assuming selection is immutable - if (selection !== this.oldSelection) { - this.oldSelection = selection; - if (selection.kind === 'legacy') { - this.bottomTab = this.attrs.tabFactory(selection.legacySelection); - } else { - this.bottomTab = undefined; - } - } - - return this.bottomTab?.renderPanel(); - } - - // Note: Must be called after render() - isLoading(): boolean { - return this.bottomTab?.isLoading() ?? false; - } -} /** * This adapter wraps a BottomTab, converting it to work with the Tab API.