| // Copyright (C) 2026 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 type {TrackEventDetailsPanel} from '../../public/details_panel'; | |
| import type {TrackEventSelection} from '../../public/selection'; | |
| import type {Trace} from '../../public/trace'; | |
| import {DetailsShell} from '../../widgets/details_shell'; | |
| import {Section} from '../../widgets/section'; | |
| import {Tree, TreeNode} from '../../widgets/tree'; | |
| import {Anchor} from '../../widgets/anchor'; | |
| import {Icons} from '../../base/semantic_icons'; | |
| import {Timestamp} from '../../components/widgets/timestamp'; | |
| import {DurationWidget} from '../../components/widgets/duration'; | |
| import {exists} from '../../base/utils'; | |
| import {NUM_NULL} from '../../trace_processor/query_result'; | |
| import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups'; | |
| import {findMainThreadTrackUri, scrollToTrackAndSelect} from './navigate'; | |
| interface StartupInfo { | |
| package: string; | |
| startupType: string | null; | |
| upid: number | null; | |
| mainThreadTrackId: number | null; | |
| } | |
| export class StartupDetailsPanel implements TrackEventDetailsPanel { | |
| private startup?: StartupInfo; | |
| private selection?: TrackEventSelection; | |
| private isLoading = true; | |
| constructor(private readonly trace: Trace) {} | |
| async load(sel: TrackEventSelection): Promise<void> { | |
| this.isLoading = true; | |
| this.selection = sel; | |
| // The selection object is enriched with dataset columns by | |
| // getSelectionDetails(). Extract the startup-specific fields. | |
| const extra = sel as unknown as { | |
| name: string; | |
| startup_type: string | null; | |
| upid: number | null; | |
| }; | |
| // Query for the main thread track ID so we can navigate like the deeplink. | |
| let mainThreadTrackId: number | null = null; | |
| if (extra.upid != null) { | |
| const result = await this.trace.engine.query(` | |
| SELECT tt.id AS main_thread_track_id | |
| FROM thread t | |
| JOIN thread_track tt ON t.utid = tt.utid | |
| WHERE t.upid = ${extra.upid} AND t.is_main_thread = 1 | |
| LIMIT 1 | |
| `); | |
| const it = result.iter({main_thread_track_id: NUM_NULL}); | |
| if (it.valid()) { | |
| mainThreadTrackId = it.main_thread_track_id; | |
| } | |
| } | |
| this.startup = { | |
| package: extra.name, | |
| startupType: extra.startup_type, | |
| upid: extra.upid, | |
| mainThreadTrackId, | |
| }; | |
| this.isLoading = false; | |
| } | |
| render(): m.Children { | |
| if (this.isLoading || !this.startup || !this.selection) { | |
| return m(DetailsShell, { | |
| title: 'Android App Startup', | |
| description: 'Loading...', | |
| }); | |
| } | |
| const {package: pkg, startupType, upid} = this.startup; | |
| const sel = this.selection; | |
| return m( | |
| DetailsShell, | |
| {title: 'Android App Startup'}, | |
| m( | |
| Section, | |
| {title: 'Details'}, | |
| m( | |
| Tree, | |
| m(TreeNode, {left: 'Package', right: pkg}), | |
| exists(startupType) && | |
| m(TreeNode, {left: 'Startup Type', right: startupType}), | |
| m(TreeNode, { | |
| left: 'Start time', | |
| right: m(Timestamp, {trace: this.trace, ts: sel.ts}), | |
| }), | |
| exists(sel.dur) && | |
| sel.dur > 0n && | |
| m(TreeNode, { | |
| left: 'Duration', | |
| right: m(DurationWidget, {trace: this.trace, dur: sel.dur}), | |
| }), | |
| ), | |
| ), | |
| upid != null && | |
| m( | |
| Section, | |
| {title: 'Actions'}, | |
| m( | |
| Anchor, | |
| { | |
| icon: Icons.GoTo, | |
| onclick: () => this.goToProcess(), | |
| title: 'Go to process', | |
| }, | |
| 'Go to process', | |
| ), | |
| ), | |
| ); | |
| } | |
| private goToProcess() { | |
| if (this.startup?.upid == null || !this.selection) return; | |
| const processGroups = this.trace.plugins.getPlugin( | |
| ProcessThreadGroupsPlugin, | |
| ) as ProcessThreadGroupsPlugin; | |
| const group = processGroups.getGroupForProcess(this.startup.upid); | |
| if (!group?.uri) return; | |
| group.expand(); | |
| const tracksToSelect: string[] = []; | |
| if (this.startup.mainThreadTrackId != null) { | |
| const mainThreadUri = findMainThreadTrackUri( | |
| this.trace, | |
| this.startup.mainThreadTrackId, | |
| ); | |
| if (mainThreadUri) { | |
| tracksToSelect.push(mainThreadUri); | |
| } | |
| } | |
| scrollToTrackAndSelect( | |
| this.trace, | |
| group.uri, | |
| tracksToSelect, | |
| this.selection.ts, | |
| this.selection.dur ?? 0n, | |
| ); | |
| } | |
| } |