blob: a133cea1b15ceebd6e51751783e9e000ff2f1d31 [file]
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {AsyncLimiter} from '../base/async_limiter';
import {defer} from '../base/deferred';
import {assertExists, assertTrue} from '../base/assert';
import {ServiceWorkerController} from '../frontend/service_worker_controller';
import type {App} from '../public/app';
import type {SqlPackage} from '../public/extra_sql_packages';
import type {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
import type {Raf} from '../public/raf';
import type {RouteArgs} from '../public/route_schema';
import type {Setting} from '../public/settings';
import type {TraceStream} from '../public/stream';
import type {DurationPrecision, TimestampFormat} from '../public/timeline';
import type {NewEngineMode} from '../trace_processor/engine';
import {type AnalyticsInternal, initAnalytics} from './analytics_impl';
import {
type CommandInvocation,
CommandManagerImpl,
type Macro,
} from './command_manager';
import {featureFlags} from './feature_flags';
import {loadTrace} from './load_trace';
import {OmniboxManagerImpl} from './omnibox_manager';
import {PageManagerImpl} from './page_manager';
import {PerfManager} from './perf_manager';
import {PluginManagerImpl} from './plugin_manager';
import {raf} from './raf_scheduler';
import {Router} from './router';
import type {SettingsManagerImpl} from './settings_manager';
import {SidebarManagerImpl} from './sidebar_manager';
import type {SerializedAppState} from './state_serialization_schema';
import type {TraceImpl} from './trace_impl';
import type {TraceArrayBufferSource, TraceSource} from './trace_source';
import {TaskTrackerImpl} from '../frontend/task_tracker/task_tracker';
import type {Embedder} from './embedder/embedder';
import {createEmbedder} from './embedder/create_embedder';
export type OpenTraceArrayBufArgs = Omit<
Omit<TraceArrayBufferSource, 'type'>,
'serializedAppState'
>;
// The args that frontend/index.ts passes when calling AppImpl.initialize().
// This is to deal with injections that would otherwise cause circular deps.
export interface AppInitArgs {
readonly initialRouteArgs: RouteArgs;
readonly settingsManager: SettingsManagerImpl;
readonly timestampFormatSetting: Setting<TimestampFormat>;
readonly durationPrecisionSetting: Setting<DurationPrecision>;
readonly timezoneOverrideSetting: Setting<string>;
readonly analyticsSetting: Setting<boolean>;
readonly startupCommandsSetting: Setting<CommandInvocation[]>;
readonly enforceStartupCommandAllowlistSetting: Setting<boolean>;
}
/**
* Handles the global state of the ui, for anything that is not related to a
* specific trace. This is always available even before a trace is loaded (in
* contrast to TraceContext, which is bound to the lifetime of a trace).
* There is only one instance in total of this class (see instance()).
* This class is only exposed to TraceImpl, nobody else should refer to this
* and should use AppImpl instead.
*/
export class AppImpl implements App {
readonly omnibox = new OmniboxManagerImpl();
readonly commands = new CommandManagerImpl(this.omnibox);
readonly pages: PageManagerImpl;
readonly sidebar: SidebarManagerImpl;
readonly plugins: PluginManagerImpl;
readonly perfDebugging = new PerfManager();
readonly analytics: AnalyticsInternal;
readonly serviceWorkerController = new ServiceWorkerController();
readonly taskTracker = new TaskTrackerImpl();
httpRpc = {
newEngineMode: 'USE_HTTP_RPC_IF_AVAILABLE' as NewEngineMode,
httpRpcAvailable: false,
};
initialRouteArgs: RouteArgs;
isLoadingTrace = false; // Set when calling openTrace().
readonly initArgs: AppInitArgs;
readonly embeddedMode: boolean;
readonly testingMode: boolean;
readonly openTraceAsyncLimiter = new AsyncLimiter();
readonly settings: SettingsManagerImpl;
readonly embedder: Embedder;
// The current active trace (if any).
private _activeTrace: TraceImpl | undefined;
// Extra SQL packages injected from extensions.
private _sqlPackagesPromises = new Array<
Promise<ReadonlyArray<SqlPackage>>
>();
// Protobuf descriptor sets as Base64-encoded strings injected from extensions.
private _protoDescriptorsPromises = new Array<
Promise<ReadonlyArray<string>>
>();
// Command macros. Injected from extensions.
private _macrosPromises = new Array<
Promise<ReadonlyArray<Macro & {source?: string}>>
>();
// Initializes the singleton instance - must be called only once and before
// AppImpl.instance is used.
static initialize(initArgs: AppInitArgs): AppImpl {
assertTrue(AppImpl._instance === undefined);
AppImpl._instance = new AppImpl(initArgs);
return AppImpl._instance;
}
// Singleton.
private static _instance: AppImpl;
static get instance(): AppImpl {
return assertExists(AppImpl._instance);
}
readonly timestampFormat: Setting<TimestampFormat>;
readonly durationPrecision: Setting<DurationPrecision>;
readonly timezoneOverride: Setting<string>;
readonly startupCommandsSetting: Setting<CommandInvocation[]>;
readonly enforceStartupCommandAllowlistSetting: Setting<boolean>;
private _isInternalUser?: boolean;
// This constructor is invoked only once, when frontend/index.ts invokes
// AppMainImpl.initialize().
private constructor(initArgs: AppInitArgs) {
this.timestampFormat = initArgs.timestampFormatSetting;
this.durationPrecision = initArgs.durationPrecisionSetting;
this.timezoneOverride = initArgs.timezoneOverrideSetting;
this.startupCommandsSetting = initArgs.startupCommandsSetting;
this.enforceStartupCommandAllowlistSetting =
initArgs.enforceStartupCommandAllowlistSetting;
this.settings = initArgs.settingsManager;
this.initArgs = initArgs;
this.initialRouteArgs = initArgs.initialRouteArgs;
this.embeddedMode = this.initialRouteArgs.mode === 'embedded';
this.testingMode =
self.location !== undefined &&
self.location.search.indexOf('testing=1') >= 0;
this.sidebar = new SidebarManagerImpl({
disabled: this.embeddedMode,
hidden: this.initialRouteArgs.hideSidebar,
});
this.embedder = createEmbedder();
this.plugins = new PluginManagerImpl(this.embedder.defaultPlugins);
this.analytics = initAnalytics(
this.testingMode,
this.embeddedMode,
initArgs.analyticsSetting.get(),
this.embedder.analyticsId,
);
this.pages = new PageManagerImpl(this.analytics);
}
setActiveTrace(trace: TraceImpl) {
this.closeCurrentTrace();
this._activeTrace = trace;
}
closeCurrentTrace() {
this.omnibox.reset(/* focus= */ false);
if (this._activeTrace) {
// This will trigger the unregistration of trace-scoped commands and
// sidebar menuitems (and few similar things).
this._activeTrace[Symbol.dispose]();
this._activeTrace = undefined;
}
}
get isInternalUser() {
if (this._isInternalUser === undefined) {
this._isInternalUser = localStorage.getItem('isInternalUser') === '1';
}
return this._isInternalUser;
}
setIsInternalUser(promise: Promise<boolean>) {
promise.then((value) => {
this._isInternalUser = value;
localStorage.setItem('isInternalUser', value ? '1' : '0');
raf.scheduleFullRedraw();
});
}
get trace(): TraceImpl | undefined {
return this._activeTrace;
}
get raf(): Raf {
return raf;
}
get featureFlags(): FeatureFlagManager {
return {
register: (settings: FlagSettings) => featureFlags.register(settings),
};
}
openTraceFromFile(file: File) {
return this.openTrace({type: 'FILE', file});
}
openTraceFromMultipleFiles(files: ReadonlyArray<File>) {
return this.openTrace({type: 'MULTIPLE_FILES', files});
}
openTraceFromUrl(url: string, serializedAppState?: SerializedAppState) {
return this.openTrace({type: 'URL', url, serializedAppState});
}
openTraceFromStream(stream: TraceStream) {
return this.openTrace({type: 'STREAM', stream});
}
openTraceFromBuffer(
args: OpenTraceArrayBufArgs,
serializedAppState?: SerializedAppState,
) {
return this.openTrace({...args, type: 'ARRAY_BUFFER', serializedAppState});
}
openTraceFromHttpRpc() {
return this.openTrace({type: 'HTTP_RPC'});
}
private async openTrace(src: TraceSource): Promise<TraceImpl> {
if (src.type === 'ARRAY_BUFFER' && src.buffer instanceof Uint8Array) {
// Even though the type of `buffer` is ArrayBuffer, it's possible to
// accidentally pass a Uint8Array here, because the interface of
// Uint8Array is compatible with ArrayBuffer. That can cause subtle bugs
// in TraceStream when creating chunks out of it (see b/390473162).
// So if we get a Uint8Array in input, convert it into an actual
// ArrayBuffer, as various parts of the codebase assume that this is a
// pure ArrayBuffer, and not a logical view of it with a byteOffset > 0.
if (
src.buffer.byteOffset === 0 &&
src.buffer.byteLength === src.buffer.buffer.byteLength
) {
src = {...src, buffer: src.buffer.buffer};
} else {
src = {...src, buffer: src.buffer.slice().buffer};
}
}
const result = defer<TraceImpl>();
// Rationale for asyncLimiter: openTrace takes several seconds and involves
// a long sequence of async tasks (e.g. invoking plugins' onLoad()). These
// tasks cannot overlap if the user opens traces in rapid succession, as
// they will mess up the state of registries. So once we start, we must
// complete trace loading (we don't bother supporting cancellations. If the
// user is too bothered, they can reload the tab).
await this.openTraceAsyncLimiter.schedule(async () => {
// Wait for extras parsing descriptors to be loaded
// via is_internal_user.js. This prevents a race condition where
// trace loading would otherwise begin before this data is available.
this.closeCurrentTrace();
this.isLoadingTrace = true;
try {
// loadTrace() in trace_loader.ts will do the following:
// - Create a new engine.
// - Pump the data from the TraceSource into the engine.
// - Do the initial queries to build the TraceImpl object
// - Call AppImpl.setActiveTrace(TraceImpl)
// - Continue with the trace loading logic (track decider, plugins, etc)
// - Resolve the promise when everything is done.
const trace = await loadTrace(this, src);
this.omnibox.reset(/* focus= */ false);
// loadTrace() internally will call setActiveTrace() and change our
// _currentTrace in the middle of its ececution. We cannot wait for
// loadTrace to be finished before setting it because some internal
// implementation details of loadTrace() rely on that trace to be current
// to work properly (mainly the router hash uuid).
result.resolve(trace);
} catch (error) {
result.reject(error);
} finally {
this.isLoadingTrace = false;
raf.scheduleFullRedraw();
}
});
return result;
}
navigate(newHash: string): void {
Router.navigate(newHash);
}
addSqlPackages(
args: ReadonlyArray<SqlPackage> | Promise<ReadonlyArray<SqlPackage>>,
) {
this._sqlPackagesPromises.push(Promise.resolve(args));
}
async sqlPackages(): Promise<ReadonlyArray<SqlPackage>> {
return Promise.all(this._sqlPackagesPromises).then((pkgs) =>
pkgs.flatMap((p) => p),
);
}
addProtoDescriptors(
args: ReadonlyArray<string> | Promise<ReadonlyArray<string>>,
) {
this._protoDescriptorsPromises.push(Promise.resolve(args));
}
async protoDescriptors(): Promise<ReadonlyArray<string>> {
return Promise.all(this._protoDescriptorsPromises).then((desc) =>
desc.flatMap((d) => d),
);
}
addMacros(
args:
| ReadonlyArray<Macro & {source?: string}>
| Promise<ReadonlyArray<Macro & {source?: string}>>,
) {
this._macrosPromises.push(Promise.resolve(args));
}
async macros(): Promise<ReadonlyArray<Macro & {source?: string}>> {
const macrosArray = await Promise.all(this._macrosPromises);
return macrosArray.flat();
}
}