blob: 78d9b1a7d11357fdae1f2e1b27224e8d8ae5074c [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 {BigintMath} from '../base/bigint_math';
import {assertExists} from '../base/logging';
import {Actions, DeferredAction} from '../common/actions';
import {AggregateData} from '../common/aggregation_data';
import {Args} from '../common/arg_types';
import {
ConversionJobName,
ConversionJobStatus,
} from '../common/conversion_jobs';
import {Engine} from '../common/engine';
import {
HighPrecisionTime,
HighPrecisionTimeSpan,
} from '../common/high_precision_time';
import {MetricResult} from '../common/metric_data';
import {CurrentSearchResults, SearchSummary} from '../common/search_data';
import {onSelectionChanged} from '../common/selection_observer';
import {CallsiteInfo, EngineConfig, ProfileType, State} from '../common/state';
import {
Span,
TPDuration,
TPTime,
tpTimeFromSeconds,
TPTimeSpan,
} from '../common/time';
import {Analytics, initAnalytics} from './analytics';
import {BottomTabList} from './bottom_tab';
import {FrontendLocalState} from './frontend_local_state';
import {setPerfHooks} from './perf';
import {RafScheduler} from './raf_scheduler';
import {Router} from './router';
import {ServiceWorkerController} from './service_worker_controller';
import {SliceSqlId, TPTimestamp} from './sql_types';
import {createStore, Store} from './store';
import {PxSpan, TimeScale} from './time_scale';
type Dispatch = (action: DeferredAction) => void;
type TrackDataStore = Map<string, {}>;
type QueryResultsStore = Map<string, {}|undefined>;
type AggregateDataStore = Map<string, AggregateData>;
type Description = Map<string, string>;
export interface SliceDetails {
ts?: TPTime;
absTime?: string;
dur?: TPDuration;
threadTs?: TPTime;
threadDur?: TPDuration;
priority?: number;
endState?: string|null;
cpu?: number;
id?: number;
threadStateId?: number;
utid?: number;
wakeupTs?: TPTime;
wakerUtid?: number;
wakerCpu?: number;
category?: string;
name?: string;
tid?: number;
threadName?: string;
pid?: number;
processName?: string;
uid?: number;
packageName?: string;
versionCode?: number;
args?: Args;
description?: Description;
}
export interface FlowPoint {
trackId: number;
sliceName: string;
sliceCategory: string;
sliceId: SliceSqlId;
sliceStartTs: TPTimestamp;
sliceEndTs: TPTimestamp;
// Thread and process info. Only set in sliceSelected not in areaSelected as
// the latter doesn't display per-flow info and it'd be a waste to join
// additional tables for undisplayed info in that case. Nothing precludes
// adding this in a future iteration however.
threadName: string;
processName: string;
depth: number;
// TODO(altimin): Ideally we should have a generic mechanism for allowing to
// customise the name here, but for now we are hardcording a few
// Chrome-specific bits in the query here.
sliceChromeCustomName?: string;
}
export interface Flow {
id: number;
begin: FlowPoint;
end: FlowPoint;
dur: TPDuration;
category?: string;
name?: string;
}
export interface CounterDetails {
startTime?: TPTime;
value?: number;
delta?: number;
duration?: TPDuration;
name?: string;
}
export interface ThreadStateDetails {
ts?: TPTime;
dur?: TPDuration;
}
export interface FlamegraphDetails {
type?: ProfileType;
id?: number;
start?: TPTime;
dur?: TPDuration;
pids?: number[];
upids?: number[];
flamegraph?: CallsiteInfo[];
expandedCallsite?: CallsiteInfo;
viewingOption?: string;
expandedId?: number;
// isInAreaSelection is true if a flamegraph is part of the current area
// selection.
isInAreaSelection?: boolean;
// When heap_graph_non_finalized_graph has a count >0, we mark the graph
// as incomplete.
graphIncomplete?: boolean;
}
export interface CpuProfileDetails {
id?: number;
ts?: TPTime;
utid?: number;
stack?: CallsiteInfo[];
}
export interface QuantizedLoad {
start: TPTime;
end: TPTime;
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>;
export interface FtraceEvent {
id: number;
ts: TPTime;
name: string;
cpu: number;
thread: string|null;
process: string|null;
args: string;
}
export interface FtracePanelData {
events: FtraceEvent[];
offset: number;
numEvents: number; // Number of events in the visible window
}
export interface FtraceStat {
name: string;
count: number;
}
function getRoot() {
// Works out the root directory where the content should be served from
// e.g. `http://origin/v1.2.3/`.
const script = document.currentScript as HTMLScriptElement;
// Needed for DOM tests, that do not have script element.
if (script === null) {
return '';
}
let root = script.src;
root = root.substr(0, root.lastIndexOf('/') + 1);
return root;
}
/**
* Global accessors for state/dispatch in the frontend.
*/
class Globals {
readonly root = getRoot();
bottomTabList?: BottomTabList = undefined;
private _testing = false;
private _dispatch?: Dispatch = undefined;
private _store?: Store<State>;
private _frontendLocalState?: FrontendLocalState = undefined;
private _rafScheduler?: RafScheduler = undefined;
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 _queryResults?: QueryResultsStore = undefined;
private _overviewStore?: OverviewStore = undefined;
private _aggregateDataStore?: AggregateDataStore = undefined;
private _threadMap?: ThreadMap = undefined;
private _sliceDetails?: SliceDetails = undefined;
private _threadStateDetails?: ThreadStateDetails = undefined;
private _connectedFlows?: Flow[] = undefined;
private _selectedFlows?: Flow[] = undefined;
private _visibleFlowCategories?: Map<string, boolean> = undefined;
private _counterDetails?: CounterDetails = undefined;
private _flamegraphDetails?: FlamegraphDetails = undefined;
private _cpuProfileDetails?: CpuProfileDetails = undefined;
private _numQueriesQueued = 0;
private _bufferUsage?: number = undefined;
private _recordingLog?: string = undefined;
private _traceErrors?: number = undefined;
private _metricError?: string = undefined;
private _metricResult?: MetricResult = undefined;
private _jobStatus?: Map<ConversionJobName, ConversionJobStatus> = undefined;
private _router?: Router = undefined;
private _embeddedMode?: boolean = undefined;
private _hideSidebar?: boolean = undefined;
private _ftraceCounters?: FtraceStat[] = undefined;
private _ftracePanelData?: FtracePanelData = undefined;
// TODO(hjd): Remove once we no longer need to update UUID on redraw.
private _publishRedraw?: () => void = undefined;
private _currentSearchResults: CurrentSearchResults = {
sliceIds: new Float64Array(0),
tsStarts: new BigInt64Array(0),
utids: new Float64Array(0),
trackIds: [],
sources: [],
totalResults: 0,
};
searchSummary: SearchSummary = {
tsStarts: new BigInt64Array(0),
tsEnds: new BigInt64Array(0),
count: new Uint8Array(0),
};
engines = new Map<string, Engine>();
initialize(dispatch: Dispatch, router: Router, initialState: State) {
this._dispatch = dispatch;
this._router = router;
this._store = createStore(initialState);
this._frontendLocalState = new FrontendLocalState();
setPerfHooks(
() => this.state.perfDebug,
() => this.dispatch(Actions.togglePerfDebug({})));
this._rafScheduler = new RafScheduler();
this._rafScheduler.beforeRedraw = () =>
this.frontendLocalState.clearVisibleTracks();
this._rafScheduler.afterRedraw = () =>
this.frontendLocalState.sendVisibleTracks();
this._serviceWorkerController = new ServiceWorkerController();
this._testing =
self.location && self.location.search.indexOf('testing=1') >= 0;
this._logging = initAnalytics();
// TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
this._trackDataStore = new Map<string, {}>();
this._queryResults = new Map<string, {}>();
this._overviewStore = new Map<string, QuantizedLoad[]>();
this._aggregateDataStore = new Map<string, AggregateData>();
this._threadMap = new Map<number, ThreadDesc>();
this._sliceDetails = {};
this._connectedFlows = [];
this._selectedFlows = [];
this._visibleFlowCategories = new Map<string, boolean>();
this._counterDetails = {};
this._threadStateDetails = {};
this._flamegraphDetails = {};
this._cpuProfileDetails = {};
this.engines.clear();
}
// Only initialises the store - useful for testing.
initStore(initialState: State) {
this._store = createStore(initialState);
}
get router(): Router {
return assertExists(this._router);
}
get publishRedraw(): () => void {
return this._publishRedraw || (() => {});
}
set publishRedraw(f: () => void) {
this._publishRedraw = f;
}
get state(): State {
return assertExists(this._store).state;
}
get store(): Store<State> {
return assertExists(this._store);
}
get dispatch(): Dispatch {
return assertExists(this._dispatch);
}
dispatchMultiple(actions: DeferredAction[]): void {
const dispatch = this.dispatch;
for (const action of actions) {
dispatch(action);
}
}
get frontendLocalState() {
return assertExists(this._frontendLocalState);
}
get rafScheduler() {
return assertExists(this._rafScheduler);
}
get logging() {
return assertExists(this._logging);
}
get serviceWorkerController() {
return assertExists(this._serviceWorkerController);
}
// TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
get overviewStore(): OverviewStore {
return assertExists(this._overviewStore);
}
get trackDataStore(): TrackDataStore {
return assertExists(this._trackDataStore);
}
get queryResults(): QueryResultsStore {
return assertExists(this._queryResults);
}
get threads() {
return assertExists(this._threadMap);
}
get sliceDetails() {
return assertExists(this._sliceDetails);
}
set sliceDetails(click: SliceDetails) {
this._sliceDetails = assertExists(click);
}
get threadStateDetails() {
return assertExists(this._threadStateDetails);
}
set threadStateDetails(click: ThreadStateDetails) {
this._threadStateDetails = assertExists(click);
}
get connectedFlows() {
return assertExists(this._connectedFlows);
}
set connectedFlows(connectedFlows: Flow[]) {
this._connectedFlows = assertExists(connectedFlows);
}
get selectedFlows() {
return assertExists(this._selectedFlows);
}
set selectedFlows(selectedFlows: Flow[]) {
this._selectedFlows = assertExists(selectedFlows);
}
get visibleFlowCategories() {
return assertExists(this._visibleFlowCategories);
}
set visibleFlowCategories(visibleFlowCategories: Map<string, boolean>) {
this._visibleFlowCategories = assertExists(visibleFlowCategories);
}
get counterDetails() {
return assertExists(this._counterDetails);
}
set counterDetails(click: CounterDetails) {
this._counterDetails = assertExists(click);
}
get aggregateDataStore(): AggregateDataStore {
return assertExists(this._aggregateDataStore);
}
get flamegraphDetails() {
return assertExists(this._flamegraphDetails);
}
set flamegraphDetails(click: FlamegraphDetails) {
this._flamegraphDetails = assertExists(click);
}
get traceErrors() {
return this._traceErrors;
}
setTraceErrors(arg: number) {
this._traceErrors = arg;
}
get metricError() {
return this._metricError;
}
setMetricError(arg: string) {
this._metricError = arg;
}
get metricResult() {
return this._metricResult;
}
setMetricResult(result: MetricResult) {
this._metricResult = result;
}
get cpuProfileDetails() {
return assertExists(this._cpuProfileDetails);
}
set cpuProfileDetails(click: CpuProfileDetails) {
this._cpuProfileDetails = assertExists(click);
}
set numQueuedQueries(value: number) {
this._numQueriesQueued = value;
}
get numQueuedQueries() {
return this._numQueriesQueued;
}
get bufferUsage() {
return this._bufferUsage;
}
get recordingLog() {
return this._recordingLog;
}
get currentSearchResults() {
return this._currentSearchResults;
}
set currentSearchResults(results: CurrentSearchResults) {
this._currentSearchResults = results;
}
get hasFtrace(): boolean {
return Boolean(this._ftraceCounters && this._ftraceCounters.length > 0);
}
get ftraceCounters(): FtraceStat[]|undefined {
return this._ftraceCounters;
}
set ftraceCounters(value: FtraceStat[]|undefined) {
this._ftraceCounters = value;
}
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;
}
setAggregateData(kind: string, data: AggregateData) {
this.aggregateDataStore.set(kind, data);
}
getCurResolution(): TPDuration {
// Truncate the resolution to the closest power of 2 (in nanosecond space).
// We choose to work in ns space because resolution is consumed be track
// controllers for quantization and they rely on resolution to be a power
// of 2 in nanosecond form. This is property does not hold if we work in
// second space.
//
// This effectively means the resolution changes approximately every 6 zoom
// levels. Logic: each zoom level represents a delta of 0.1 * (visible
// window span). Therefore, zooming out by six levels is 1.1^6 ~= 2.
// Similarily, zooming in six levels is 0.9^6 ~= 0.5.
const timeScale = this.frontendLocalState.visibleTimeScale;
// TODO(b/186265930): Remove once fixed:
if (timeScale.pxSpan.delta === 0) {
console.error(`b/186265930: Bad pxToSec suppressed`);
return BigintMath.bitFloor(tpTimeFromSeconds(1000));
}
const timePerPx = timeScale.pxDeltaToDuration(this.quantPx);
return BigintMath.bitFloor(timePerPx.toTPTime('floor'));
}
getCurrentEngine(): EngineConfig|undefined {
return this.state.engine;
}
get ftracePanelData(): FtracePanelData|undefined {
return this._ftracePanelData;
}
set ftracePanelData(data: FtracePanelData|undefined) {
this._ftracePanelData = data;
}
makeSelection(
action: DeferredAction<{}>, tab: string|null = 'current_selection') {
const previousState = this.state;
// A new selection should cancel the current search selection.
globals.dispatch(Actions.setSearchIndex({index: -1}));
if (action.type === 'deselect') {
globals.dispatch(Actions.setCurrentTab({tab: undefined}));
} else if (tab !== null) {
globals.dispatch(Actions.setCurrentTab({tab: tab}));
}
globals.dispatch(action);
// HACK(stevegolton + altimin): This is a workaround to allow passing the
// next tab state to the Bottom Tab API
if (this.state.currentSelection !== previousState.currentSelection) {
// TODO(altimin): Currently we are not triggering this when changing
// the set of selected tracks via toggling per-track checkboxes.
// Fix that.
onSelectionChanged(
this.state.currentSelection ?? undefined,
tab === 'current_selection');
}
}
resetForTesting() {
this._dispatch = undefined;
this._store = undefined;
this._frontendLocalState = undefined;
this._rafScheduler = undefined;
this._serviceWorkerController = undefined;
// TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
this._trackDataStore = undefined;
this._queryResults = undefined;
this._overviewStore = undefined;
this._threadMap = undefined;
this._sliceDetails = undefined;
this._threadStateDetails = undefined;
this._aggregateDataStore = undefined;
this._numQueriesQueued = 0;
this._metricResult = undefined;
this._currentSearchResults = {
sliceIds: new Float64Array(0),
tsStarts: new BigInt64Array(0),
utids: new Float64Array(0),
trackIds: [],
sources: [],
totalResults: 0,
};
}
// 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;
}
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() {
this._rafScheduler!.shutdown();
}
// Get a timescale that covers the entire trace
getTraceTimeScale(pxSpan: PxSpan): TimeScale {
const {start, end} = this.state.traceTime;
const traceTime = HighPrecisionTimeSpan.fromTpTime(start, end);
return TimeScale.fromHPTimeSpan(traceTime, pxSpan);
}
// Get the trace time bounds
stateTraceTime(): Span<HighPrecisionTime> {
const {start, end} = this.state.traceTime;
return HighPrecisionTimeSpan.fromTpTime(start, end);
}
stateTraceTimeTP(): Span<TPTime> {
const {start, end} = this.state.traceTime;
return new TPTimeSpan(start, end);
}
// Get the state version of the visible time bounds
stateVisibleTime(): Span<TPTime> {
const {start, end} = this.state.frontendLocalState.visibleState;
return new TPTimeSpan(start, end);
}
// How many pixels to use for one quanta of horizontal resolution
get quantPx(): number {
const quantPx = (self as {} as {quantPx: number | undefined}).quantPx;
if (quantPx) {
return quantPx;
} else {
// Default to 1px per quanta if not defined
return 1;
}
}
}
export const globals = new Globals();