| // 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 {DisposableStack} from '../base/disposable_stack'; |
| import {createStore, type Migrate, type Store} from '../base/store'; |
| import {TimelineImpl} from './timeline'; |
| import type {Command} from '../public/commands'; |
| import type {Trace} from '../public/trace'; |
| import type {ScrollToArgs} from '../public/scroll_helper'; |
| import type {Engine, EngineBase} from '../trace_processor/engine'; |
| import type {CommandManagerImpl} from './command_manager'; |
| import {NoteManagerImpl} from './note_manager'; |
| import type {OmniboxManagerImpl} from './omnibox_manager'; |
| import {SearchManagerImpl} from './search_manager'; |
| import {SelectionManagerImpl} from './selection_manager'; |
| import type {SidebarManagerImpl} from './sidebar_manager'; |
| import {TabManagerImpl} from './tab_manager'; |
| import {TrackManagerImpl} from './track_manager'; |
| import {WorkspaceManagerImpl} from './workspace_manager'; |
| import type {SidebarMenuItem} from '../public/sidebar'; |
| import {ScrollHelper} from './scroll_helper'; |
| import type {Selection, SelectionOpts} from '../public/selection'; |
| import type {SearchResult} from '../public/search'; |
| import {FlowManager} from './flow_manager'; |
| import type {AppImpl, OpenTraceArrayBufArgs} from './app_impl'; |
| import type {PluginManagerImpl} from './plugin_manager'; |
| import type {RouteArgs} from '../public/route_schema'; |
| import type {Analytics} from '../public/analytics'; |
| import {fetchWithProgress} from '../base/http_utils'; |
| import type {TraceInfoImpl} from './trace_info_impl'; |
| import type {PageHandler, PageManager} from '../public/page'; |
| import {createProxy} from '../base/utils'; |
| import type {PageManagerImpl} from './page_manager'; |
| import type {FeatureFlagManager, FlagSettings} from '../public/feature_flag'; |
| import type {SerializedAppState} from './state_serialization_schema'; |
| import {featureFlags} from './feature_flags'; |
| import {EvtSource} from '../base/events'; |
| import type {Raf} from '../public/raf'; |
| import {StatusbarManagerImpl} from './statusbar_manager'; |
| import type {SettingDescriptor} from '../public/settings'; |
| import type {SettingsManagerImpl} from './settings_manager'; |
| import {MinimapManagerImpl} from './minimap_manager'; |
| import {InitialPageManagerImpl} from './initial_page_manager'; |
| import type {TraceStream} from '../public/stream'; |
| import type {OmniboxModeDescriptor} from '../public/omnibox'; |
| |
| /** |
| * This implementation provides the plugin access to trace related resources, |
| * such as the engine and the store. This exists for the whole duration a plugin |
| * is active AND a trace is loaded. |
| * There are N+1 instances of this for each trace, one for each plugin plus one |
| * for the core. |
| */ |
| export class TraceImpl implements Trace, Disposable { |
| readonly engine: Engine; |
| readonly search: SearchManagerImpl; |
| readonly selection: SelectionManagerImpl; |
| readonly tabs = new TabManagerImpl(); |
| readonly timeline: TimelineImpl; |
| readonly traceInfo: TraceInfoImpl; |
| readonly tracks = new TrackManagerImpl(); |
| readonly workspaces = new WorkspaceManagerImpl(); |
| readonly notes = new NoteManagerImpl(); |
| readonly flows: FlowManager; |
| readonly scrollHelper: ScrollHelper; |
| readonly trash = new DisposableStack(); |
| readonly onTraceReady = new EvtSource<void>(); |
| readonly statusbar = new StatusbarManagerImpl(); |
| readonly minimap = new MinimapManagerImpl(); |
| readonly initialPage = new InitialPageManagerImpl(); |
| readonly loadingErrors: string[] = []; |
| readonly app: AppImpl; |
| readonly store = createStore<Record<string, unknown>>({}); |
| |
| // Do we need this? |
| readonly pluginSerializableState = createStore<{[key: string]: {}}>({}); |
| |
| constructor(app: AppImpl, engine: EngineBase, traceInfo: TraceInfoImpl) { |
| this.app = app; |
| this.engine = engine; |
| this.trash.use(engine); |
| this.traceInfo = traceInfo; |
| |
| this.timeline = new TimelineImpl( |
| traceInfo, |
| app.timestampFormat, |
| app.durationPrecision, |
| app.timezoneOverride, |
| ); |
| |
| this.scrollHelper = new ScrollHelper( |
| this.timeline, |
| this.workspaces, |
| this.tracks, |
| ); |
| |
| this.selection = new SelectionManagerImpl( |
| this.engine, |
| this.tracks, |
| this.notes, |
| this.scrollHelper, |
| this.onSelectionChange.bind(this), |
| ); |
| |
| this.notes.onNoteDeleted = (noteId) => { |
| if ( |
| this.selection.selection.kind === 'note' && |
| this.selection.selection.id === noteId |
| ) { |
| this.selection.clearSelection(); |
| } |
| }; |
| |
| this.flows = new FlowManager( |
| engine.getProxy('FlowManager'), |
| this.tracks, |
| this.selection, |
| ); |
| |
| this.search = new SearchManagerImpl({ |
| timeline: this.timeline, |
| trackManager: this.tracks, |
| engine: this.engine, |
| workspace: this.workspaces.currentWorkspace, |
| onResultStep: this.onResultStep.bind(this), |
| }); |
| |
| // CommandManager is global. Here we intercept the registerCommand() because |
| // we want any commands registered via the Trace interface to be |
| // unregistered when the trace unloads (before a new trace is loaded) to |
| // avoid ending up with duplicate commands. |
| this.commandMgrProxy = createProxy(app.commands, { |
| registerCommand: (cmd: Command) => { |
| const disposable = app.commands.registerCommand(cmd); |
| this.trash.use(disposable); |
| return disposable; |
| }, |
| registerMacro: (macro, source) => { |
| const disposable = app.commands.registerMacro(macro, source); |
| this.trash.use(disposable); |
| return disposable; |
| }, |
| }); |
| |
| // Likewise, remove all trace-scoped sidebar entries when the trace unloads. |
| this.sidebarProxy = createProxy(app.sidebar, { |
| addMenuItem: (menuItem: SidebarMenuItem) => { |
| const disposable = app.sidebar.addMenuItem(menuItem); |
| this.trash.use(disposable); |
| return disposable; |
| }, |
| }); |
| |
| this.pageMgrProxy = createProxy(app.pages, { |
| registerPage: (pageHandler: PageHandler) => { |
| const disposable = app.pages.registerPage(pageHandler); |
| this.trash.use(disposable); |
| return disposable; |
| }, |
| }); |
| |
| this.settingsProxy = createProxy(app.settings, { |
| register: <T>(setting: SettingDescriptor<T>) => { |
| const disposable = app.settings.register(setting); |
| this.trash.use(disposable); |
| return disposable; |
| }, |
| }); |
| |
| this.omniboxProxy = createProxy(app.omnibox, { |
| registerMode: (descriptor: OmniboxModeDescriptor) => { |
| const disposable = app.omnibox.registerMode(descriptor); |
| this.trash.use(disposable); |
| return disposable; |
| }, |
| }); |
| } |
| |
| // This method wires up changes to selection to side effects on search and |
| // tabs. This is to avoid entangling too many dependencies between managers. |
| private onSelectionChange(selection: Selection, opts: SelectionOpts) { |
| const {clearSearch = true, switchToCurrentSelectionTab = true} = opts; |
| if (clearSearch) { |
| this.search.reset(); |
| } |
| if (switchToCurrentSelectionTab && selection.kind !== 'empty') { |
| this.tabs.showCurrentSelectionTab(); |
| } |
| |
| this.flows.updateFlows(selection); |
| } |
| |
| private onResultStep(searchResult: SearchResult) { |
| this.selection.selectSearchResult(searchResult); |
| } |
| |
| [Symbol.dispose]() { |
| this.trash.dispose(); |
| } |
| |
| private readonly commandMgrProxy: CommandManagerImpl; |
| private readonly sidebarProxy: SidebarManagerImpl; |
| private readonly pageMgrProxy: PageManagerImpl; |
| private readonly settingsProxy: SettingsManagerImpl; |
| private readonly omniboxProxy: OmniboxManagerImpl; |
| |
| scrollTo(where: ScrollToArgs): void { |
| this.scrollHelper.scrollTo(where); |
| } |
| |
| async getTraceFile(): Promise<Blob> { |
| const src = this.traceInfo.source; |
| if (this.traceInfo.downloadable) { |
| if (src.type === 'ARRAY_BUFFER') { |
| return new Blob([src.buffer]); |
| } else if (src.type === 'FILE') { |
| return src.file; |
| } else if (src.type === 'URL') { |
| return await fetchWithProgress(src.url, (progressPercent: number) => |
| this.omnibox.showStatusMessage( |
| `Downloading trace ${progressPercent}%`, |
| ), |
| ); |
| } |
| } |
| // Not available in HTTP+RPC mode. Rather than propagating an undefined, |
| // show a graceful error (the ERR:trace_src will be intercepted by |
| // error_dialog.ts). We expect all users of this feature to not be able to |
| // do anything useful if we returned undefined (other than showing the same |
| // dialog). |
| // The caller was supposed to check that traceInfo.downloadable === true |
| // before calling this. Throwing while downloadable is true is a bug. |
| throw new Error(`Cannot getTraceFile(${src.type})`); |
| } |
| |
| get trace() { |
| return this; |
| } |
| |
| get taskTracker() { |
| return this.app.taskTracker; |
| } |
| |
| get currentWorkspace() { |
| return this.workspaces.currentWorkspace; |
| } |
| |
| get defaultWorkspace() { |
| return this.workspaces.defaultWorkspace; |
| } |
| |
| get commands(): CommandManagerImpl { |
| return this.commandMgrProxy; |
| } |
| |
| get sidebar(): SidebarManagerImpl { |
| return this.sidebarProxy; |
| } |
| |
| get pages(): PageManager { |
| return this.pageMgrProxy; |
| } |
| |
| get omnibox(): OmniboxManagerImpl { |
| return this.omniboxProxy; |
| } |
| |
| get plugins(): PluginManagerImpl { |
| return this.app.plugins; |
| } |
| |
| get analytics(): Analytics { |
| return this.app.analytics; |
| } |
| |
| get initialRouteArgs(): RouteArgs { |
| return this.app.initialRouteArgs; |
| } |
| |
| get featureFlags(): FeatureFlagManager { |
| return { |
| register: (settings: FlagSettings) => featureFlags.register(settings), |
| }; |
| } |
| |
| get raf(): Raf { |
| return this.app.raf; |
| } |
| |
| navigate(newHash: string): void { |
| this.app.navigate(newHash); |
| } |
| |
| openTraceFromFile(file: File) { |
| return this.app.openTraceFromFile(file); |
| } |
| |
| openTraceFromUrl(url: string, serializedAppState?: SerializedAppState) { |
| return this.app.openTraceFromUrl(url, serializedAppState); |
| } |
| |
| openTraceFromStream(stream: TraceStream) { |
| return this.app.openTraceFromStream(stream); |
| } |
| |
| openTraceFromBuffer( |
| args: OpenTraceArrayBufArgs, |
| serializedAppState?: SerializedAppState, |
| ) { |
| return this.app.openTraceFromBuffer(args, serializedAppState); |
| } |
| |
| closeCurrentTrace(): void { |
| this.app.closeCurrentTrace(); |
| } |
| |
| get settings(): SettingsManagerImpl { |
| return this.settingsProxy; |
| } |
| |
| get isInternalUser(): boolean { |
| return this.app.isInternalUser; |
| } |
| |
| get perfDebugging() { |
| return this.app.perfDebugging; |
| } |
| |
| mountStore<T>(id: string, migrate: Migrate<T>): Store<T> { |
| return this.store.createSubStore([id], migrate); |
| } |
| } |
| |
| // A convenience interface to inject the App in Mithril components. |
| export interface TraceImplAttrs { |
| trace: TraceImpl; |
| } |
| |
| export interface OptionalTraceImplAttrs { |
| trace?: TraceImpl; |
| } |