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