xref: /aosp_15_r20/external/perfetto/ui/src/core/app_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 {assertExists, assertTrue} from '../base/logging';
16import {App} from '../public/app';
17import {TraceContext, TraceImpl} from './trace_impl';
18import {CommandManagerImpl} from './command_manager';
19import {OmniboxManagerImpl} from './omnibox_manager';
20import {raf} from './raf_scheduler';
21import {SidebarManagerImpl} from './sidebar_manager';
22import {PluginManagerImpl} from './plugin_manager';
23import {NewEngineMode} from '../trace_processor/engine';
24import {RouteArgs} from '../public/route_schema';
25import {SqlPackage} from '../public/extra_sql_packages';
26import {SerializedAppState} from './state_serialization_schema';
27import {PostedTrace, TraceSource} from './trace_source';
28import {loadTrace} from './load_trace';
29import {CORE_PLUGIN_ID} from './plugin_manager';
30import {Router} from './router';
31import {AnalyticsInternal, initAnalytics} from './analytics_impl';
32import {createProxy, getOrCreate} from '../base/utils';
33import {PageManagerImpl} from './page_manager';
34import {PageHandler} from '../public/page';
35import {PerfManager} from './perf_manager';
36import {ServiceWorkerController} from '../frontend/service_worker_controller';
37import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
38import {featureFlags} from './feature_flags';
39
40// The args that frontend/index.ts passes when calling AppImpl.initialize().
41// This is to deal with injections that would otherwise cause circular deps.
42export interface AppInitArgs {
43  initialRouteArgs: RouteArgs;
44}
45
46/**
47 * Handles the global state of the ui, for anything that is not related to a
48 * specific trace. This is always available even before a trace is loaded (in
49 * contrast to TraceContext, which is bound to the lifetime of a trace).
50 * There is only one instance in total of this class (see instance()).
51 * This class is only exposed to TraceImpl, nobody else should refer to this
52 * and should use AppImpl instead.
53 */
54export class AppContext {
55  // The per-plugin instances of AppImpl (including the CORE_PLUGIN one).
56  private readonly pluginInstances = new Map<string, AppImpl>();
57  readonly commandMgr = new CommandManagerImpl();
58  readonly omniboxMgr = new OmniboxManagerImpl();
59  readonly pageMgr = new PageManagerImpl();
60  readonly sidebarMgr: SidebarManagerImpl;
61  readonly pluginMgr: PluginManagerImpl;
62  readonly perfMgr = new PerfManager();
63  readonly analytics: AnalyticsInternal;
64  readonly serviceWorkerController: ServiceWorkerController;
65  httpRpc = {
66    newEngineMode: 'USE_HTTP_RPC_IF_AVAILABLE' as NewEngineMode,
67    httpRpcAvailable: false,
68  };
69  initialRouteArgs: RouteArgs;
70  isLoadingTrace = false; // Set when calling openTrace().
71  readonly initArgs: AppInitArgs;
72  readonly embeddedMode: boolean;
73  readonly testingMode: boolean;
74
75  // This is normally empty and is injected with extra google-internal packages
76  // via is_internal_user.js
77  extraSqlPackages: SqlPackage[] = [];
78
79  // The currently open trace.
80  currentTrace?: TraceContext;
81
82  private static _instance: AppContext;
83
84  static initialize(initArgs: AppInitArgs): AppContext {
85    assertTrue(AppContext._instance === undefined);
86    return (AppContext._instance = new AppContext(initArgs));
87  }
88
89  static get instance(): AppContext {
90    return assertExists(AppContext._instance);
91  }
92
93  // This constructor is invoked only once, when frontend/index.ts invokes
94  // AppMainImpl.initialize().
95  private constructor(initArgs: AppInitArgs) {
96    this.initArgs = initArgs;
97    this.initialRouteArgs = initArgs.initialRouteArgs;
98    this.serviceWorkerController = new ServiceWorkerController();
99    this.embeddedMode = this.initialRouteArgs.mode === 'embedded';
100    this.testingMode =
101      self.location !== undefined &&
102      self.location.search.indexOf('testing=1') >= 0;
103    this.sidebarMgr = new SidebarManagerImpl({
104      disabled: this.embeddedMode,
105      hidden: this.initialRouteArgs.hideSidebar,
106    });
107    this.analytics = initAnalytics(this.testingMode, this.embeddedMode);
108    this.pluginMgr = new PluginManagerImpl({
109      forkForPlugin: (pluginId) => this.forPlugin(pluginId),
110      get trace() {
111        return AppImpl.instance.trace;
112      },
113    });
114  }
115
116  // Gets or creates an instance of AppImpl backed by the current AppContext
117  // for the given plugin.
118  forPlugin(pluginId: string) {
119    return getOrCreate(this.pluginInstances, pluginId, () => {
120      return new AppImpl(this, pluginId);
121    });
122  }
123
124  closeCurrentTrace() {
125    this.omniboxMgr.reset(/* focus= */ false);
126
127    if (this.currentTrace !== undefined) {
128      // This will trigger the unregistration of trace-scoped commands and
129      // sidebar menuitems (and few similar things).
130      this.currentTrace[Symbol.dispose]();
131      this.currentTrace = undefined;
132    }
133  }
134
135  // Called by trace_loader.ts soon after it has created a new TraceImpl.
136  setActiveTrace(traceCtx: TraceContext) {
137    // In 99% this closeCurrentTrace() call is not needed because the real one
138    // is performed by openTrace() in this file. However in some rare cases we
139    // might end up loading a trace while another one is still loading, and this
140    // covers races in that case.
141    this.closeCurrentTrace();
142    this.currentTrace = traceCtx;
143  }
144}
145
146/*
147 * Every plugin gets its own instance. This is how we keep track
148 * what each plugin is doing and how we can blame issues on particular
149 * plugins.
150 * The instance exists for the whole duration a plugin is active.
151 */
152
153export class AppImpl implements App {
154  readonly pluginId: string;
155  private readonly appCtx: AppContext;
156  private readonly pageMgrProxy: PageManagerImpl;
157
158  // Invoked by frontend/index.ts.
159  static initialize(args: AppInitArgs) {
160    AppContext.initialize(args).forPlugin(CORE_PLUGIN_ID);
161  }
162
163  // Gets access to the one instance that the core can use. Note that this is
164  // NOT the only instance, as other AppImpl instance will be created for each
165  // plugin.
166  static get instance(): AppImpl {
167    return AppContext.instance.forPlugin(CORE_PLUGIN_ID);
168  }
169
170  // Only called by AppContext.forPlugin().
171  constructor(appCtx: AppContext, pluginId: string) {
172    this.appCtx = appCtx;
173    this.pluginId = pluginId;
174
175    this.pageMgrProxy = createProxy(this.appCtx.pageMgr, {
176      registerPage(pageHandler: PageHandler): Disposable {
177        return appCtx.pageMgr.registerPage({
178          ...pageHandler,
179          pluginId,
180        });
181      },
182    });
183  }
184
185  forPlugin(pluginId: string): AppImpl {
186    return this.appCtx.forPlugin(pluginId);
187  }
188
189  get commands(): CommandManagerImpl {
190    return this.appCtx.commandMgr;
191  }
192
193  get sidebar(): SidebarManagerImpl {
194    return this.appCtx.sidebarMgr;
195  }
196
197  get omnibox(): OmniboxManagerImpl {
198    return this.appCtx.omniboxMgr;
199  }
200
201  get plugins(): PluginManagerImpl {
202    return this.appCtx.pluginMgr;
203  }
204
205  get analytics(): AnalyticsInternal {
206    return this.appCtx.analytics;
207  }
208
209  get pages(): PageManagerImpl {
210    return this.pageMgrProxy;
211  }
212
213  get trace(): TraceImpl | undefined {
214    return this.appCtx.currentTrace?.forPlugin(this.pluginId);
215  }
216
217  scheduleFullRedraw(force?: 'force'): void {
218    raf.scheduleFullRedraw(force);
219  }
220
221  get httpRpc() {
222    return this.appCtx.httpRpc;
223  }
224
225  get initialRouteArgs(): RouteArgs {
226    return this.appCtx.initialRouteArgs;
227  }
228
229  get featureFlags(): FeatureFlagManager {
230    return {
231      register: (settings: FlagSettings) => featureFlags.register(settings),
232    };
233  }
234
235  openTraceFromFile(file: File): void {
236    this.openTrace({type: 'FILE', file});
237  }
238
239  openTraceFromUrl(url: string, serializedAppState?: SerializedAppState) {
240    this.openTrace({type: 'URL', url, serializedAppState});
241  }
242
243  openTraceFromBuffer(postMessageArgs: PostedTrace): void {
244    this.openTrace({type: 'ARRAY_BUFFER', ...postMessageArgs});
245  }
246
247  openTraceFromHttpRpc(): void {
248    this.openTrace({type: 'HTTP_RPC'});
249  }
250
251  private async openTrace(src: TraceSource) {
252    this.appCtx.closeCurrentTrace();
253    this.appCtx.isLoadingTrace = true;
254    try {
255      // loadTrace() in trace_loader.ts will do the following:
256      // - Create a new engine.
257      // - Pump the data from the TraceSource into the engine.
258      // - Do the initial queries to build the TraceImpl object
259      // - Call AppImpl.setActiveTrace(TraceImpl)
260      // - Continue with the trace loading logic (track decider, plugins, etc)
261      // - Resolve the promise when everything is done.
262      await loadTrace(this, src);
263      this.omnibox.reset(/* focus= */ false);
264      // loadTrace() internally will call setActiveTrace() and change our
265      // _currentTrace in the middle of its ececution. We cannot wait for
266      // loadTrace to be finished before setting it because some internal
267      // implementation details of loadTrace() rely on that trace to be current
268      // to work properly (mainly the router hash uuid).
269    } catch (err) {
270      this.omnibox.showStatusMessage(`${err}`);
271      throw err;
272    } finally {
273      this.appCtx.isLoadingTrace = false;
274      raf.scheduleFullRedraw();
275    }
276  }
277
278  // Called by trace_loader.ts soon after it has created a new TraceImpl.
279  setActiveTrace(traceImpl: TraceImpl) {
280    this.appCtx.setActiveTrace(traceImpl.__traceCtxForApp);
281  }
282
283  get embeddedMode(): boolean {
284    return this.appCtx.embeddedMode;
285  }
286
287  get testingMode(): boolean {
288    return this.appCtx.testingMode;
289  }
290
291  get isLoadingTrace() {
292    return this.appCtx.isLoadingTrace;
293  }
294
295  get extraSqlPackages(): SqlPackage[] {
296    return this.appCtx.extraSqlPackages;
297  }
298
299  get perfDebugging(): PerfManager {
300    return this.appCtx.perfMgr;
301  }
302
303  get serviceWorkerController(): ServiceWorkerController {
304    return this.appCtx.serviceWorkerController;
305  }
306
307  // Nothing other than TraceImpl's constructor should ever refer to this.
308  // This is necessary to avoid circular dependencies between trace_impl.ts
309  // and app_impl.ts.
310  get __appCtxForTrace() {
311    return this.appCtx;
312  }
313
314  navigate(newHash: string): void {
315    Router.navigate(newHash);
316  }
317}
318