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