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