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