blob: e0b7e7f878ca2ae38380eeb0273f228d05e16f1f [file] [log] [blame]
// Copyright (C) 2018 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 {assertExists} from '../base/logging';
import {createStore, Store} from '../base/store';
import {Time, time} from '../base/time';
import {Actions, DeferredAction} from '../common/actions';
import {CommandManagerImpl} from '../core/command_manager';
import {
ConversionJobName,
ConversionJobStatus,
} from '../common/conversion_jobs';
import {createEmptyState} from '../common/empty_state';
import {EngineConfig, State} from '../common/state';
import {TimestampFormat, timestampFormat} from '../core/timestamp_format';
import {setPerfHooks} from '../core/perf';
import {raf} from '../core/raf_scheduler';
import {ServiceWorkerController} from './service_worker_controller';
import {EngineBase} from '../trace_processor/engine';
import {HttpRpcState} from '../trace_processor/http_rpc_engine';
import type {Analytics} from './analytics';
import {SerializedAppState} from '../common/state_serialization_schema';
import {getServingRoot} from '../base/http_utils';
import {Workspace} from '../public/workspace';
import {ratelimit} from './rate_limiters';
import {
AppImpl,
setRerunControllersFunction,
TraceImpl,
} from '../core/app_trace_impl';
import {createFakeTraceImpl} from '../common/fake_trace_impl';
type DispatchMultiple = (actions: DeferredAction[]) => void;
type TrackDataStore = Map<string, {}>;
export interface QuantizedLoad {
start: time;
end: time;
load: number;
}
type OverviewStore = Map<string, QuantizedLoad[]>;
export interface ThreadDesc {
utid: number;
tid: number;
threadName: string;
pid?: number;
procName?: string;
cmdline?: string;
}
type ThreadMap = Map<number, ThreadDesc>;
interface SqlModule {
readonly name: string;
readonly sql: string;
}
interface SqlPackage {
readonly name: string;
readonly modules: SqlModule[];
}
/**
* Global accessors for state/dispatch in the frontend.
*/
class Globals {
readonly root = getServingRoot();
private _trace: TraceImpl;
private _testing = false;
private _dispatchMultiple?: DispatchMultiple = undefined;
private _store = createStore<State>(createEmptyState());
private _serviceWorkerController?: ServiceWorkerController = undefined;
private _logging?: Analytics = undefined;
private _isInternalUser: boolean | undefined = undefined;
// TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
private _trackDataStore?: TrackDataStore = undefined;
private _overviewStore?: OverviewStore = undefined;
private _threadMap?: ThreadMap = undefined;
private _numQueriesQueued = 0;
private _bufferUsage?: number = undefined;
private _recordingLog?: string = undefined;
private _traceErrors?: number = undefined;
private _metricError?: string = undefined;
private _jobStatus?: Map<ConversionJobName, ConversionJobStatus> = undefined;
private _embeddedMode?: boolean = undefined;
private _hideSidebar?: boolean = undefined;
private _hasFtrace: boolean = false;
private _currentTraceId = '';
httpRpcState: HttpRpcState = {connected: false};
showPanningHint = false;
permalinkHash?: string;
showTraceErrorPopup = true;
extraSqlPackages: SqlPackage[] = [];
get workspace(): Workspace {
return this._trace?.workspace;
}
// This is the app's equivalent of a plugin's onTraceLoad() function.
// TODO(primiano): right now this is used to inject the TracImpl class into
// globals, so it can hop consistently all its accessors to it. Once globals
// is gone, figure out what to do with createSearchOverviewTrack().
async onTraceLoad(trace: TraceImpl): Promise<void> {
this._trace = trace;
this._trace.timeline.retriggerControllersOnChange = () =>
ratelimit(() => this.store.edit(() => {}), 50);
this._currentTraceId = trace.engine.engineId;
}
// Used for permalink load by trace_controller.ts.
restoreAppStateAfterTraceLoad?: SerializedAppState;
// TODO(hjd): Remove once we no longer need to update UUID on redraw.
private _publishRedraw?: () => void = undefined;
engines = new Map<string, EngineBase>();
constructor() {
// TODO(primiano): we do this to avoid making all our members possibly
// undefined, which would cause a drama of if (!=undefined) all over the
// code. This is not pretty, but this entire file is going to be nuked from
// orbit soon.
this._trace = createFakeTraceImpl();
// We just want an empty instance of TraceImpl but don't want to mark it
// as the current trace, otherwise this will trigger the plugins' OnLoad().
AppImpl.instance.closeCurrentTrace();
setRerunControllersFunction(() =>
this.dispatch(Actions.runControllers({})),
);
}
initialize(
dispatchMultiple: DispatchMultiple,
initAnalytics: () => Analytics,
) {
this._dispatchMultiple = dispatchMultiple;
setPerfHooks(
() => this.state.perfDebug,
() => this.dispatch(Actions.togglePerfDebug({})),
);
this._serviceWorkerController = new ServiceWorkerController(
getServingRoot(),
);
this._testing =
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
self.location && self.location.search.indexOf('testing=1') >= 0;
/* eslint-enable */
// TODO(stevegolton): This is a mess. We should just inject this object in,
// instead of passing in a function. The only reason this is done like this
// is because the current implementation of initAnalytics depends on the
// state of globals.testing, so this needs to be set before we run the
// function.
this._logging = initAnalytics();
// TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
// TODO(primiano): for posterity: these assignments below are completely
// pointless and could be done as member variable initializers, as
// initialize() is only called ever once. (But then i'm going to kill this
// entire file soon).
this._trackDataStore = new Map<string, {}>();
this._overviewStore = new Map<string, QuantizedLoad[]>();
this._threadMap = new Map<number, ThreadDesc>();
this.engines.clear();
}
get publishRedraw(): () => void {
return this._publishRedraw || (() => {});
}
set publishRedraw(f: () => void) {
this._publishRedraw = f;
}
get state(): State {
return this._store.state;
}
get store(): Store<State> {
return this._store;
}
dispatch(action: DeferredAction) {
this.dispatchMultiple([action]);
}
dispatchMultiple(actions: DeferredAction[]) {
assertExists(this._dispatchMultiple)(actions);
}
get trace() {
return this._trace;
}
get timeline() {
return this._trace.timeline;
}
get searchManager() {
return this._trace.search;
}
get logging() {
return assertExists(this._logging);
}
get serviceWorkerController() {
return assertExists(this._serviceWorkerController);
}
// TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
// TODO(primiano): this should be really renamed to traceInfo, but doing so
// creates extra churn. Not worth it as we are going to get rid of this file
// soon.
get traceContext() {
return this._trace.traceInfo;
}
get overviewStore(): OverviewStore {
return assertExists(this._overviewStore);
}
get trackDataStore(): TrackDataStore {
return assertExists(this._trackDataStore);
}
get threads() {
return assertExists(this._threadMap);
}
get traceErrors() {
return this._traceErrors;
}
setTraceErrors(arg: number) {
this._traceErrors = arg;
}
get metricError() {
return this._metricError;
}
setMetricError(arg: string) {
this._metricError = arg;
}
set numQueuedQueries(value: number) {
this._numQueriesQueued = value;
}
get numQueuedQueries() {
return this._numQueriesQueued;
}
get bufferUsage() {
return this._bufferUsage;
}
get recordingLog() {
return this._recordingLog;
}
set hasFtrace(value: boolean) {
this._hasFtrace = value;
}
get hasFtrace(): boolean {
return this._hasFtrace;
}
get currentTraceId() {
return this._currentTraceId;
}
getConversionJobStatus(name: ConversionJobName): ConversionJobStatus {
return this.getJobStatusMap().get(name) ?? ConversionJobStatus.NotRunning;
}
setConversionJobStatus(name: ConversionJobName, status: ConversionJobStatus) {
const map = this.getJobStatusMap();
if (status === ConversionJobStatus.NotRunning) {
map.delete(name);
} else {
map.set(name, status);
}
}
private getJobStatusMap(): Map<ConversionJobName, ConversionJobStatus> {
if (!this._jobStatus) {
this._jobStatus = new Map();
}
return this._jobStatus;
}
get embeddedMode(): boolean {
return !!this._embeddedMode;
}
set embeddedMode(value: boolean) {
this._embeddedMode = value;
}
get hideSidebar(): boolean {
return !!this._hideSidebar;
}
set hideSidebar(value: boolean) {
this._hideSidebar = value;
}
setBufferUsage(bufferUsage: number) {
this._bufferUsage = bufferUsage;
}
setTrackData(id: string, data: {}) {
this.trackDataStore.set(id, data);
}
setRecordingLog(recordingLog: string) {
this._recordingLog = recordingLog;
}
getCurrentEngine(): EngineConfig | undefined {
return this.state.engine;
}
// This variable is set by the is_internal_user.js script if the user is a
// googler. This is used to avoid exposing features that are not ready yet
// for public consumption. The gated features themselves are not secret.
// If a user has been detected as a Googler once, make that sticky in
// localStorage, so that we keep treating them as such when they connect over
// public networks.
get isInternalUser() {
if (this._isInternalUser === undefined) {
this._isInternalUser = localStorage.getItem('isInternalUser') === '1';
}
return this._isInternalUser;
}
set isInternalUser(value: boolean) {
localStorage.setItem('isInternalUser', value ? '1' : '0');
this._isInternalUser = value;
raf.scheduleFullRedraw();
}
get testing() {
return this._testing;
}
// Used when switching to the legacy TraceViewer UI.
// Most resources are cleaned up by replacing the current |window| object,
// however pending RAFs and workers seem to outlive the |window| and need to
// be cleaned up explicitly.
shutdown() {
raf.shutdown();
}
get commandManager(): CommandManagerImpl {
return AppImpl.instance.commands;
}
get tabManager() {
return this._trace.tabs;
}
get trackManager() {
return this._trace.tracks;
}
get selectionManager() {
return this._trace.selection;
}
get noteManager() {
return this._trace.notes;
}
// Offset between t=0 and the configured time domain.
timestampOffset(): time {
const fmt = timestampFormat();
switch (fmt) {
case TimestampFormat.Timecode:
case TimestampFormat.Seconds:
case TimestampFormat.Milliseoncds:
case TimestampFormat.Microseconds:
return this._trace.traceInfo.start;
case TimestampFormat.TraceNs:
case TimestampFormat.TraceNsLocale:
return Time.ZERO;
case TimestampFormat.UTC:
return this._trace.traceInfo.utcOffset;
case TimestampFormat.TraceTz:
return this._trace.traceInfo.traceTzOffset;
default:
const x: never = fmt;
throw new Error(`Unsupported format ${x}`);
}
}
// Convert absolute time to domain time.
toDomainTime(ts: time): time {
return Time.sub(ts, this.timestampOffset());
}
}
export const globals = new Globals();