xref: /aosp_15_r20/external/perfetto/ui/src/core/trace_impl.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {DisposableStack} from '../base/disposable_stack';
16import {createStore, Migrate, Store} from '../base/store';
17import {TimelineImpl} from './timeline';
18import {Command} from '../public/command';
19import {Trace} from '../public/trace';
20import {ScrollToArgs, setScrollToFunction} from '../public/scroll_helper';
21import {TrackDescriptor} from '../public/track';
22import {EngineBase, EngineProxy} from '../trace_processor/engine';
23import {CommandManagerImpl} from './command_manager';
24import {NoteManagerImpl} from './note_manager';
25import {OmniboxManagerImpl} from './omnibox_manager';
26import {SearchManagerImpl} from './search_manager';
27import {SelectionManagerImpl} from './selection_manager';
28import {SidebarManagerImpl} from './sidebar_manager';
29import {TabManagerImpl} from './tab_manager';
30import {TrackManagerImpl} from './track_manager';
31import {WorkspaceManagerImpl} from './workspace_manager';
32import {SidebarMenuItem} from '../public/sidebar';
33import {ScrollHelper} from './scroll_helper';
34import {Selection, SelectionOpts} from '../public/selection';
35import {SearchResult} from '../public/search';
36import {PivotTableManager} from './pivot_table_manager';
37import {FlowManager} from './flow_manager';
38import {AppContext, AppImpl} from './app_impl';
39import {PluginManagerImpl} from './plugin_manager';
40import {RouteArgs} from '../public/route_schema';
41import {CORE_PLUGIN_ID} from './plugin_manager';
42import {Analytics} from '../public/analytics';
43import {getOrCreate} from '../base/utils';
44import {fetchWithProgress} from '../base/http_utils';
45import {TraceInfoImpl} from './trace_info_impl';
46import {PageHandler, PageManager} from '../public/page';
47import {createProxy} from '../base/utils';
48import {PageManagerImpl} from './page_manager';
49import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
50import {featureFlags} from './feature_flags';
51import {SerializedAppState} from './state_serialization_schema';
52import {PostedTrace} from './trace_source';
53import {PerfManager} from './perf_manager';
54import {EvtSource} from '../base/events';
55
56/**
57 * Handles the per-trace state of the UI
58 * There is an instance of this class per each trace loaded, and typically
59 * between 0 and 1 instances in total (% brief moments while we swap traces).
60 * 90% of the app state live here, including the Engine.
61 * This is the underlying storage for AppImpl, which instead has one instance
62 * per trace per plugin.
63 */
64export class TraceContext implements Disposable {
65  private readonly pluginInstances = new Map<string, TraceImpl>();
66  readonly appCtx: AppContext;
67  readonly engine: EngineBase;
68  readonly omniboxMgr = new OmniboxManagerImpl();
69  readonly searchMgr: SearchManagerImpl;
70  readonly selectionMgr: SelectionManagerImpl;
71  readonly tabMgr = new TabManagerImpl();
72  readonly timeline: TimelineImpl;
73  readonly traceInfo: TraceInfoImpl;
74  readonly trackMgr = new TrackManagerImpl();
75  readonly workspaceMgr = new WorkspaceManagerImpl();
76  readonly noteMgr = new NoteManagerImpl();
77  readonly flowMgr: FlowManager;
78  readonly pluginSerializableState = createStore<{[key: string]: {}}>({});
79  readonly scrollHelper: ScrollHelper;
80  readonly pivotTableMgr;
81  readonly trash = new DisposableStack();
82  readonly onTraceReady = new EvtSource<void>();
83
84  // List of errors that were encountered while loading the trace by the TS
85  // code. These are on top of traceInfo.importErrors, which is a summary of
86  // what TraceProcessor reports on the stats table at import time.
87  readonly loadingErrors: string[] = [];
88
89  constructor(gctx: AppContext, engine: EngineBase, traceInfo: TraceInfoImpl) {
90    this.appCtx = gctx;
91    this.engine = engine;
92    this.trash.use(engine);
93    this.traceInfo = traceInfo;
94    this.timeline = new TimelineImpl(traceInfo);
95
96    this.scrollHelper = new ScrollHelper(
97      this.traceInfo,
98      this.timeline,
99      this.workspaceMgr.currentWorkspace,
100      this.trackMgr,
101    );
102
103    this.selectionMgr = new SelectionManagerImpl(
104      this.engine,
105      this.trackMgr,
106      this.noteMgr,
107      this.scrollHelper,
108      this.onSelectionChange.bind(this),
109    );
110
111    this.noteMgr.onNoteDeleted = (noteId) => {
112      if (
113        this.selectionMgr.selection.kind === 'note' &&
114        this.selectionMgr.selection.id === noteId
115      ) {
116        this.selectionMgr.clear();
117      }
118    };
119
120    this.pivotTableMgr = new PivotTableManager(
121      engine.getProxy('PivotTableManager'),
122    );
123
124    this.flowMgr = new FlowManager(
125      engine.getProxy('FlowManager'),
126      this.trackMgr,
127      this.selectionMgr,
128    );
129
130    this.searchMgr = new SearchManagerImpl({
131      timeline: this.timeline,
132      trackManager: this.trackMgr,
133      engine: this.engine,
134      workspace: this.workspaceMgr.currentWorkspace,
135      onResultStep: this.onResultStep.bind(this),
136    });
137  }
138
139  // This method wires up changes to selection to side effects on search and
140  // tabs. This is to avoid entangling too many dependencies between managers.
141  private onSelectionChange(selection: Selection, opts: SelectionOpts) {
142    const {clearSearch = true, switchToCurrentSelectionTab = true} = opts;
143    if (clearSearch) {
144      this.searchMgr.reset();
145    }
146    if (switchToCurrentSelectionTab && selection.kind !== 'empty') {
147      this.tabMgr.showCurrentSelectionTab();
148    }
149
150    if (selection.kind === 'area') {
151      this.pivotTableMgr.setSelectionArea(selection);
152    }
153
154    this.flowMgr.updateFlows(selection);
155  }
156
157  private onResultStep(searchResult: SearchResult) {
158    this.selectionMgr.selectSearchResult(searchResult);
159  }
160
161  // Gets or creates an instance of TraceImpl backed by the current TraceContext
162  // for the given plugin.
163  forPlugin(pluginId: string) {
164    return getOrCreate(this.pluginInstances, pluginId, () => {
165      const appForPlugin = this.appCtx.forPlugin(pluginId);
166      return new TraceImpl(appForPlugin, this);
167    });
168  }
169
170  // Called by AppContext.closeCurrentTrace().
171  [Symbol.dispose]() {
172    this.trash.dispose();
173  }
174}
175
176/**
177 * This implementation provides the plugin access to trace related resources,
178 * such as the engine and the store. This exists for the whole duration a plugin
179 * is active AND a trace is loaded.
180 * There are N+1 instances of this for each trace, one for each plugin plus one
181 * for the core.
182 */
183export class TraceImpl implements Trace {
184  private readonly appImpl: AppImpl;
185  private readonly traceCtx: TraceContext;
186
187  // This is not the original Engine base, rather an EngineProxy based on the
188  // same engineBase.
189  private readonly engineProxy: EngineProxy;
190  private readonly trackMgrProxy: TrackManagerImpl;
191  private readonly commandMgrProxy: CommandManagerImpl;
192  private readonly sidebarProxy: SidebarManagerImpl;
193  private readonly pageMgrProxy: PageManagerImpl;
194
195  // This is called by TraceController when loading a new trace, soon after the
196  // engine has been set up. It obtains a new TraceImpl for the core. From that
197  // we can fork sibling instances (i.e. bound to the same TraceContext) for
198  // the various plugins.
199  static createInstanceForCore(
200    appImpl: AppImpl,
201    engine: EngineBase,
202    traceInfo: TraceInfoImpl,
203  ): TraceImpl {
204    const traceCtx = new TraceContext(
205      appImpl.__appCtxForTrace,
206      engine,
207      traceInfo,
208    );
209    return traceCtx.forPlugin(CORE_PLUGIN_ID);
210  }
211
212  // Only called by TraceContext.forPlugin().
213  constructor(appImpl: AppImpl, ctx: TraceContext) {
214    const pluginId = appImpl.pluginId;
215    this.appImpl = appImpl;
216    this.traceCtx = ctx;
217    const traceUnloadTrash = ctx.trash;
218
219    // Invalidate all the engine proxies when the TraceContext is destroyed.
220    this.engineProxy = ctx.engine.getProxy(pluginId);
221    traceUnloadTrash.use(this.engineProxy);
222
223    // Intercept the registerTrack() method to inject the pluginId into tracks.
224    this.trackMgrProxy = createProxy(ctx.trackMgr, {
225      registerTrack(trackDesc: TrackDescriptor): Disposable {
226        return ctx.trackMgr.registerTrack({...trackDesc, pluginId});
227      },
228    });
229
230    // CommandManager is global. Here we intercept the registerCommand() because
231    // we want any commands registered via the Trace interface to be
232    // unregistered when the trace unloads (before a new trace is loaded) to
233    // avoid ending up with duplicate commands.
234    this.commandMgrProxy = createProxy(ctx.appCtx.commandMgr, {
235      registerCommand(cmd: Command): Disposable {
236        const disposable = appImpl.commands.registerCommand(cmd);
237        traceUnloadTrash.use(disposable);
238        return disposable;
239      },
240    });
241
242    // Likewise, remove all trace-scoped sidebar entries when the trace unloads.
243    this.sidebarProxy = createProxy(ctx.appCtx.sidebarMgr, {
244      addMenuItem(menuItem: SidebarMenuItem): Disposable {
245        const disposable = appImpl.sidebar.addMenuItem(menuItem);
246        traceUnloadTrash.use(disposable);
247        return disposable;
248      },
249    });
250
251    this.pageMgrProxy = createProxy(ctx.appCtx.pageMgr, {
252      registerPage(pageHandler: PageHandler): Disposable {
253        const disposable = appImpl.pages.registerPage({
254          ...pageHandler,
255          pluginId: appImpl.pluginId,
256        });
257        traceUnloadTrash.use(disposable);
258        return disposable;
259      },
260    });
261
262    // TODO(primiano): remove this injection once we plumb Trace everywhere.
263    setScrollToFunction((x: ScrollToArgs) => ctx.scrollHelper.scrollTo(x));
264  }
265
266  scrollTo(where: ScrollToArgs): void {
267    this.traceCtx.scrollHelper.scrollTo(where);
268  }
269
270  // Creates an instance of TraceImpl backed by the same TraceContext for
271  // another plugin. This is effectively a way to "fork" the core instance and
272  // create the N instances for plugins.
273  forkForPlugin(pluginId: string) {
274    return this.traceCtx.forPlugin(pluginId);
275  }
276
277  mountStore<T>(migrate: Migrate<T>): Store<T> {
278    return this.traceCtx.pluginSerializableState.createSubStore(
279      [this.pluginId],
280      migrate,
281    );
282  }
283
284  getPluginStoreForSerialization() {
285    return this.traceCtx.pluginSerializableState;
286  }
287
288  async getTraceFile(): Promise<Blob> {
289    const src = this.traceInfo.source;
290    if (this.traceInfo.downloadable) {
291      if (src.type === 'ARRAY_BUFFER') {
292        return new Blob([src.buffer]);
293      } else if (src.type === 'FILE') {
294        return src.file;
295      } else if (src.type === 'URL') {
296        return await fetchWithProgress(src.url, (progressPercent: number) =>
297          this.omnibox.showStatusMessage(
298            `Downloading trace ${progressPercent}%`,
299          ),
300        );
301      }
302    }
303    // Not available in HTTP+RPC mode. Rather than propagating an undefined,
304    // show a graceful error (the ERR:trace_src will be intercepted by
305    // error_dialog.ts). We expect all users of this feature to not be able to
306    // do anything useful if we returned undefined (other than showing the same
307    // dialog).
308    // The caller was supposed to check that traceInfo.downloadable === true
309    // before calling this. Throwing while downloadable is true is a bug.
310    throw new Error(`Cannot getTraceFile(${src.type})`);
311  }
312
313  get openerPluginArgs(): {[key: string]: unknown} | undefined {
314    const traceSource = this.traceCtx.traceInfo.source;
315    if (traceSource.type !== 'ARRAY_BUFFER') {
316      return undefined;
317    }
318    const pluginArgs = traceSource.pluginArgs;
319    return (pluginArgs ?? {})[this.pluginId];
320  }
321
322  get trace() {
323    return this;
324  }
325
326  get engine() {
327    return this.engineProxy;
328  }
329
330  get timeline() {
331    return this.traceCtx.timeline;
332  }
333
334  get tracks() {
335    return this.trackMgrProxy;
336  }
337
338  get tabs() {
339    return this.traceCtx.tabMgr;
340  }
341
342  get workspace() {
343    return this.traceCtx.workspaceMgr.currentWorkspace;
344  }
345
346  get workspaces() {
347    return this.traceCtx.workspaceMgr;
348  }
349
350  get search() {
351    return this.traceCtx.searchMgr;
352  }
353
354  get selection() {
355    return this.traceCtx.selectionMgr;
356  }
357
358  get traceInfo(): TraceInfoImpl {
359    return this.traceCtx.traceInfo;
360  }
361
362  get notes() {
363    return this.traceCtx.noteMgr;
364  }
365
366  get pivotTable() {
367    return this.traceCtx.pivotTableMgr;
368  }
369
370  get flows() {
371    return this.traceCtx.flowMgr;
372  }
373
374  get loadingErrors(): ReadonlyArray<string> {
375    return this.traceCtx.loadingErrors;
376  }
377
378  addLoadingError(err: string) {
379    this.traceCtx.loadingErrors.push(err);
380  }
381
382  // App interface implementation.
383
384  get pluginId(): string {
385    return this.appImpl.pluginId;
386  }
387
388  get commands(): CommandManagerImpl {
389    return this.commandMgrProxy;
390  }
391
392  get sidebar(): SidebarManagerImpl {
393    return this.sidebarProxy;
394  }
395
396  get pages(): PageManager {
397    return this.pageMgrProxy;
398  }
399
400  get omnibox(): OmniboxManagerImpl {
401    return this.appImpl.omnibox;
402  }
403
404  get plugins(): PluginManagerImpl {
405    return this.appImpl.plugins;
406  }
407
408  get analytics(): Analytics {
409    return this.appImpl.analytics;
410  }
411
412  get initialRouteArgs(): RouteArgs {
413    return this.appImpl.initialRouteArgs;
414  }
415
416  get featureFlags(): FeatureFlagManager {
417    return {
418      register: (settings: FlagSettings) => featureFlags.register(settings),
419    };
420  }
421
422  scheduleFullRedraw(): void {
423    this.appImpl.scheduleFullRedraw();
424  }
425
426  navigate(newHash: string): void {
427    this.appImpl.navigate(newHash);
428  }
429
430  openTraceFromFile(file: File): void {
431    this.appImpl.openTraceFromFile(file);
432  }
433
434  openTraceFromUrl(url: string, serializedAppState?: SerializedAppState) {
435    this.appImpl.openTraceFromUrl(url, serializedAppState);
436  }
437
438  openTraceFromBuffer(args: PostedTrace): void {
439    this.appImpl.openTraceFromBuffer(args);
440  }
441
442  get onTraceReady() {
443    return this.traceCtx.onTraceReady;
444  }
445
446  get perfDebugging(): PerfManager {
447    return this.appImpl.perfDebugging;
448  }
449
450  get trash(): DisposableStack {
451    return this.traceCtx.trash;
452  }
453
454  // Nothing other than AppImpl should ever refer to this, hence the __ name.
455  get __traceCtxForApp() {
456    return this.traceCtx;
457  }
458}
459
460// A convenience interface to inject the App in Mithril components.
461export interface TraceImplAttrs {
462  trace: TraceImpl;
463}
464
465export interface OptionalTraceImplAttrs {
466  trace?: TraceImpl;
467}
468