| // 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, Migrate, Store} from '../base/store'; |
| import {TimelineImpl} from './timeline'; |
| import {Command} from '../public/command'; |
| import {EventListeners, Trace} from '../public/trace'; |
| import {ScrollToArgs, setScrollToFunction} from '../public/scroll_helper'; |
| import {TrackDescriptor} from '../public/track'; |
| import {EngineBase, EngineProxy} from '../trace_processor/engine'; |
| import {CommandManagerImpl} from './command_manager'; |
| import {NoteManagerImpl} from './note_manager'; |
| import {OmniboxManagerImpl} from './omnibox_manager'; |
| import {SearchManagerImpl} from './search_manager'; |
| import {SelectionManagerImpl} from './selection_manager'; |
| import {SidebarManagerImpl} from './sidebar_manager'; |
| import {TabManagerImpl} from './tab_manager'; |
| import {TrackManagerImpl} from './track_manager'; |
| import {WorkspaceManagerImpl} from './workspace_manager'; |
| import {SidebarMenuItem} from '../public/sidebar'; |
| import {ScrollHelper} from './scroll_helper'; |
| import {Selection, SelectionOpts} from '../public/selection'; |
| import {SearchResult} from '../public/search'; |
| import {PivotTableManager} from './pivot_table_manager'; |
| import {FlowManager} from './flow_manager'; |
| import {AppContext, AppImpl} from './app_impl'; |
| import {PluginManagerImpl} from './plugin_manager'; |
| import {RouteArgs} from '../public/route_schema'; |
| import {CORE_PLUGIN_ID} from './plugin_manager'; |
| import {Analytics} from '../public/analytics'; |
| import {getOrCreate} from '../base/utils'; |
| import {fetchWithProgress} from '../base/http_utils'; |
| import {TraceInfoImpl} from './trace_info_impl'; |
| import {PageHandler, PageManager} from '../public/page'; |
| import {createProxy} from '../base/utils'; |
| import {PageManagerImpl} from './page_manager'; |
| import {FeatureFlagManager, FlagSettings} from '../public/feature_flag'; |
| import {featureFlags} from './feature_flags'; |
| import {SerializedAppState} from './state_serialization_schema'; |
| import {PostedTrace} from './trace_source'; |
| |
| /** |
| * Handles the per-trace state of the UI |
| * There is an instance of this class per each trace loaded, and typically |
| * between 0 and 1 instances in total (% brief moments while we swap traces). |
| * 90% of the app state live here, including the Engine. |
| * This is the underlying storage for AppImpl, which instead has one instance |
| * per trace per plugin. |
| */ |
| export class TraceContext implements Disposable { |
| private readonly pluginInstances = new Map<string, TraceImpl>(); |
| readonly appCtx: AppContext; |
| readonly engine: EngineBase; |
| readonly omniboxMgr = new OmniboxManagerImpl(); |
| readonly searchMgr: SearchManagerImpl; |
| readonly selectionMgr: SelectionManagerImpl; |
| readonly tabMgr = new TabManagerImpl(); |
| readonly timeline: TimelineImpl; |
| readonly traceInfo: TraceInfoImpl; |
| readonly trackMgr = new TrackManagerImpl(); |
| readonly workspaceMgr = new WorkspaceManagerImpl(); |
| readonly noteMgr = new NoteManagerImpl(); |
| readonly flowMgr: FlowManager; |
| readonly pluginSerializableState = createStore<{[key: string]: {}}>({}); |
| readonly scrollHelper: ScrollHelper; |
| readonly pivotTableMgr; |
| readonly trash = new DisposableStack(); |
| readonly eventListeners = new Map<keyof EventListeners, Array<unknown>>(); |
| |
| // List of errors that were encountered while loading the trace by the TS |
| // code. These are on top of traceInfo.importErrors, which is a summary of |
| // what TraceProcessor reports on the stats table at import time. |
| readonly loadingErrors: string[] = []; |
| |
| constructor(gctx: AppContext, engine: EngineBase, traceInfo: TraceInfoImpl) { |
| this.appCtx = gctx; |
| this.engine = engine; |
| this.trash.use(engine); |
| this.traceInfo = traceInfo; |
| this.timeline = new TimelineImpl(traceInfo); |
| |
| this.scrollHelper = new ScrollHelper( |
| this.traceInfo, |
| this.timeline, |
| this.workspaceMgr.currentWorkspace, |
| this.trackMgr, |
| ); |
| |
| this.selectionMgr = new SelectionManagerImpl( |
| this.engine, |
| this.trackMgr, |
| this.noteMgr, |
| this.scrollHelper, |
| this.onSelectionChange.bind(this), |
| ); |
| |
| this.noteMgr.onNoteDeleted = (noteId) => { |
| if ( |
| this.selectionMgr.selection.kind === 'note' && |
| this.selectionMgr.selection.id === noteId |
| ) { |
| this.selectionMgr.clear(); |
| } |
| }; |
| |
| this.pivotTableMgr = new PivotTableManager( |
| engine.getProxy('PivotTableManager'), |
| ); |
| |
| this.flowMgr = new FlowManager( |
| engine.getProxy('FlowManager'), |
| this.trackMgr, |
| this.selectionMgr, |
| ); |
| |
| this.searchMgr = new SearchManagerImpl({ |
| timeline: this.timeline, |
| trackManager: this.trackMgr, |
| engine: this.engine, |
| workspace: this.workspaceMgr.currentWorkspace, |
| onResultStep: this.onResultStep.bind(this), |
| }); |
| } |
| |
| // 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.searchMgr.reset(); |
| } |
| if (switchToCurrentSelectionTab && selection.kind !== 'empty') { |
| this.tabMgr.showCurrentSelectionTab(); |
| } |
| |
| if (selection.kind === 'area') { |
| this.pivotTableMgr.setSelectionArea(selection); |
| } |
| |
| this.flowMgr.updateFlows(selection); |
| } |
| |
| private onResultStep(searchResult: SearchResult) { |
| this.selectionMgr.selectSearchResult(searchResult); |
| } |
| |
| // Gets or creates an instance of TraceImpl backed by the current TraceContext |
| // for the given plugin. |
| forPlugin(pluginId: string) { |
| return getOrCreate(this.pluginInstances, pluginId, () => { |
| const appForPlugin = this.appCtx.forPlugin(pluginId); |
| return new TraceImpl(appForPlugin, this); |
| }); |
| } |
| |
| // Called by AppContext.closeCurrentTrace(). |
| [Symbol.dispose]() { |
| this.trash.dispose(); |
| } |
| } |
| |
| /** |
| * 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 { |
| private readonly appImpl: AppImpl; |
| private readonly traceCtx: TraceContext; |
| |
| // This is not the original Engine base, rather an EngineProxy based on the |
| // same engineBase. |
| private readonly engineProxy: EngineProxy; |
| private readonly trackMgrProxy: TrackManagerImpl; |
| private readonly commandMgrProxy: CommandManagerImpl; |
| private readonly sidebarProxy: SidebarManagerImpl; |
| private readonly pageMgrProxy: PageManagerImpl; |
| |
| // This is called by TraceController when loading a new trace, soon after the |
| // engine has been set up. It obtains a new TraceImpl for the core. From that |
| // we can fork sibling instances (i.e. bound to the same TraceContext) for |
| // the various plugins. |
| static createInstanceForCore( |
| appImpl: AppImpl, |
| engine: EngineBase, |
| traceInfo: TraceInfoImpl, |
| ): TraceImpl { |
| const traceCtx = new TraceContext( |
| appImpl.__appCtxForTrace, |
| engine, |
| traceInfo, |
| ); |
| return traceCtx.forPlugin(CORE_PLUGIN_ID); |
| } |
| |
| // Only called by TraceContext.forPlugin(). |
| constructor(appImpl: AppImpl, ctx: TraceContext) { |
| const pluginId = appImpl.pluginId; |
| this.appImpl = appImpl; |
| this.traceCtx = ctx; |
| const traceUnloadTrash = ctx.trash; |
| |
| // Invalidate all the engine proxies when the TraceContext is destroyed. |
| this.engineProxy = ctx.engine.getProxy(pluginId); |
| traceUnloadTrash.use(this.engineProxy); |
| |
| // Intercept the registerTrack() method to inject the pluginId into tracks. |
| this.trackMgrProxy = createProxy(ctx.trackMgr, { |
| registerTrack(trackDesc: TrackDescriptor): Disposable { |
| return ctx.trackMgr.registerTrack({...trackDesc, pluginId}); |
| }, |
| }); |
| |
| // 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(ctx.appCtx.commandMgr, { |
| registerCommand(cmd: Command): Disposable { |
| const disposable = appImpl.commands.registerCommand(cmd); |
| traceUnloadTrash.use(disposable); |
| return disposable; |
| }, |
| }); |
| |
| // Likewise, remove all trace-scoped sidebar entries when the trace unloads. |
| this.sidebarProxy = createProxy(ctx.appCtx.sidebarMgr, { |
| addMenuItem(menuItem: SidebarMenuItem): Disposable { |
| const disposable = appImpl.sidebar.addMenuItem(menuItem); |
| traceUnloadTrash.use(disposable); |
| return disposable; |
| }, |
| }); |
| |
| this.pageMgrProxy = createProxy(ctx.appCtx.pageMgr, { |
| registerPage(pageHandler: PageHandler): Disposable { |
| const disposable = appImpl.pages.registerPage({ |
| ...pageHandler, |
| pluginId: appImpl.pluginId, |
| }); |
| traceUnloadTrash.use(disposable); |
| return disposable; |
| }, |
| }); |
| |
| // TODO(primiano): remove this injection once we plumb Trace everywhere. |
| setScrollToFunction((x: ScrollToArgs) => ctx.scrollHelper.scrollTo(x)); |
| } |
| |
| scrollTo(where: ScrollToArgs): void { |
| this.traceCtx.scrollHelper.scrollTo(where); |
| } |
| |
| // Creates an instance of TraceImpl backed by the same TraceContext for |
| // another plugin. This is effectively a way to "fork" the core instance and |
| // create the N instances for plugins. |
| forkForPlugin(pluginId: string) { |
| return this.traceCtx.forPlugin(pluginId); |
| } |
| |
| mountStore<T>(migrate: Migrate<T>): Store<T> { |
| return this.traceCtx.pluginSerializableState.createSubStore( |
| [this.pluginId], |
| migrate, |
| ); |
| } |
| |
| getPluginStoreForSerialization() { |
| return this.traceCtx.pluginSerializableState; |
| } |
| |
| 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 openerPluginArgs(): {[key: string]: unknown} | undefined { |
| const traceSource = this.traceCtx.traceInfo.source; |
| if (traceSource.type !== 'ARRAY_BUFFER') { |
| return undefined; |
| } |
| const pluginArgs = traceSource.pluginArgs; |
| return (pluginArgs ?? {})[this.pluginId]; |
| } |
| |
| get trace() { |
| return this; |
| } |
| |
| get engine() { |
| return this.engineProxy; |
| } |
| |
| get timeline() { |
| return this.traceCtx.timeline; |
| } |
| |
| get tracks() { |
| return this.trackMgrProxy; |
| } |
| |
| get tabs() { |
| return this.traceCtx.tabMgr; |
| } |
| |
| get workspace() { |
| return this.traceCtx.workspaceMgr.currentWorkspace; |
| } |
| |
| get workspaces() { |
| return this.traceCtx.workspaceMgr; |
| } |
| |
| get search() { |
| return this.traceCtx.searchMgr; |
| } |
| |
| get selection() { |
| return this.traceCtx.selectionMgr; |
| } |
| |
| get traceInfo(): TraceInfoImpl { |
| return this.traceCtx.traceInfo; |
| } |
| |
| get notes() { |
| return this.traceCtx.noteMgr; |
| } |
| |
| get pivotTable() { |
| return this.traceCtx.pivotTableMgr; |
| } |
| |
| get flows() { |
| return this.traceCtx.flowMgr; |
| } |
| |
| get loadingErrors(): ReadonlyArray<string> { |
| return this.traceCtx.loadingErrors; |
| } |
| |
| addLoadingError(err: string) { |
| this.traceCtx.loadingErrors.push(err); |
| } |
| |
| // App interface implementation. |
| |
| get pluginId(): string { |
| return this.appImpl.pluginId; |
| } |
| |
| get commands(): CommandManagerImpl { |
| return this.commandMgrProxy; |
| } |
| |
| get sidebar(): SidebarManagerImpl { |
| return this.sidebarProxy; |
| } |
| |
| get pages(): PageManager { |
| return this.pageMgrProxy; |
| } |
| |
| get omnibox(): OmniboxManagerImpl { |
| return this.appImpl.omnibox; |
| } |
| |
| get plugins(): PluginManagerImpl { |
| return this.appImpl.plugins; |
| } |
| |
| get analytics(): Analytics { |
| return this.appImpl.analytics; |
| } |
| |
| get initialRouteArgs(): RouteArgs { |
| return this.appImpl.initialRouteArgs; |
| } |
| |
| get featureFlags(): FeatureFlagManager { |
| return { |
| register: (settings: FlagSettings) => featureFlags.register(settings), |
| }; |
| } |
| |
| scheduleFullRedraw(): void { |
| this.appImpl.scheduleFullRedraw(); |
| } |
| |
| navigate(newHash: string): void { |
| this.appImpl.navigate(newHash); |
| } |
| |
| openTraceFromFile(file: File): void { |
| this.appImpl.openTraceFromFile(file); |
| } |
| |
| openTraceFromUrl(url: string, serializedAppState?: SerializedAppState) { |
| this.appImpl.openTraceFromUrl(url, serializedAppState); |
| } |
| |
| openTraceFromBuffer(args: PostedTrace): void { |
| this.appImpl.openTraceFromBuffer(args); |
| } |
| |
| addEventListener<T extends keyof EventListeners>( |
| event: T, |
| callback: EventListeners[T], |
| ): void { |
| const listeners = getOrCreate( |
| this.traceCtx.eventListeners, |
| event, |
| () => [], |
| ); |
| listeners.push(callback); |
| } |
| |
| getEventListeners<T extends keyof EventListeners>( |
| event: T, |
| ): ReadonlyArray<EventListeners[T]> { |
| const listeners = this.traceCtx.eventListeners.get(event); |
| if (listeners) { |
| return listeners as ReadonlyArray<EventListeners[T]>; |
| } else { |
| return []; |
| } |
| } |
| |
| get trash(): DisposableStack { |
| return this.traceCtx.trash; |
| } |
| |
| // Nothing other than AppImpl should ever refer to this, hence the __ name. |
| get __traceCtxForApp() { |
| return this.traceCtx; |
| } |
| } |
| |
| // A convenience interface to inject the App in Mithril components. |
| export interface TraceImplAttrs { |
| trace: TraceImpl; |
| } |
| |
| export interface OptionalTraceImplAttrs { |
| trace?: TraceImpl; |
| } |