// 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 {Trace} from '../public/trace'; import {ScrollToArgs, setScrollToFunction} from '../public/scroll_helper'; import {Track} 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 {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'; import {PerfManager} from './perf_manager'; import {EvtSource} from '../base/events'; import {Raf} from '../public/raf'; /** * 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(); 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 trash = new DisposableStack(); readonly onTraceReady = new EvtSource(); // 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.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(); } 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: Track): 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(migrate: Migrate): Store { return this.traceCtx.pluginSerializableState.createSubStore( [this.pluginId], migrate, ); } getPluginStoreForSerialization() { return this.traceCtx.pluginSerializableState; } async getTraceFile(): Promise { 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 flows() { return this.traceCtx.flowMgr; } get loadingErrors(): ReadonlyArray { 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), }; } get raf(): Raf { return this.appImpl.raf; } 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); } get onTraceReady() { return this.traceCtx.onTraceReady; } get perfDebugging(): PerfManager { return this.appImpl.perfDebugging; } 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; }