| // 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 {Actions, DeferredAction} from '../common/actions'; |
| import {AggregateData} from '../common/aggregation_data'; |
| import {Args, ArgsTree} from '../common/arg_types'; |
| import { |
| ConversionJobName, |
| ConversionJobStatus |
| } from '../common/conversion_jobs'; |
| import {createEmptyState} from '../common/empty_state'; |
| import {Engine} from '../common/engine'; |
| import {MetricResult} from '../common/metric_data'; |
| import {CurrentSearchResults, SearchSummary} from '../common/search_data'; |
| import {CallsiteInfo, State} from '../common/state'; |
| import {fromNs, toNs} from '../common/time'; |
| |
| import {Analytics, initAnalytics} from './analytics'; |
| import {FrontendLocalState} from './frontend_local_state'; |
| import {PivotTableHelper} from './pivot_table_helper'; |
| import {RafScheduler} from './raf_scheduler'; |
| import {Router} from './router'; |
| import {ServiceWorkerController} from './service_worker_controller'; |
| |
| type Dispatch = (action: DeferredAction) => void; |
| type TrackDataStore = Map<string, {}>; |
| type QueryResultsStore = Map<string, {}|undefined>; |
| type PivotTableHelperStore = Map<string, PivotTableHelper>; |
| type AggregateDataStore = Map<string, AggregateData>; |
| type Description = Map<string, string>; |
| export interface SliceDetails { |
| ts?: number; |
| dur?: number; |
| thread_ts?: number; |
| thread_dur?: number; |
| priority?: number; |
| endState?: string; |
| cpu?: number; |
| id?: number; |
| threadStateId?: number; |
| utid?: number; |
| wakeupTs?: number; |
| wakerUtid?: number; |
| wakerCpu?: number; |
| category?: string; |
| name?: string; |
| args?: Args; |
| argsTree?: ArgsTree; |
| description?: Description; |
| } |
| |
| export interface FlowPoint { |
| trackId: number; |
| |
| sliceName: string; |
| sliceCategory: string; |
| sliceId: number; |
| sliceStartTs: number; |
| sliceEndTs: number; |
| |
| depth: number; |
| } |
| |
| export interface Flow { |
| id: number; |
| |
| begin: FlowPoint; |
| end: FlowPoint; |
| |
| category?: string; |
| name?: string; |
| } |
| |
| export interface CounterDetails { |
| startTime?: number; |
| value?: number; |
| delta?: number; |
| duration?: number; |
| name?: string; |
| } |
| |
| export interface ThreadStateDetails { |
| ts?: number; |
| dur?: number; |
| state?: string; |
| utid?: number; |
| cpu?: number; |
| sliceId?: number; |
| blockedFunction?: string; |
| } |
| |
| export interface FlamegraphDetails { |
| type?: string; |
| id?: number; |
| ts?: number; |
| tsNs?: number; |
| pid?: number; |
| upid?: number; |
| flamegraph?: CallsiteInfo[]; |
| expandedCallsite?: CallsiteInfo; |
| viewingOption?: string; |
| expandedId?: number; |
| } |
| |
| export interface CpuProfileDetails { |
| id?: number; |
| ts?: number; |
| utid?: number; |
| stack?: CallsiteInfo[]; |
| } |
| |
| export interface QuantizedLoad { |
| startSec: number; |
| endSec: number; |
| 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>; |
| |
| 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(); |
| |
| private _testing = false; |
| private _dispatch?: Dispatch = undefined; |
| private _state?: State = undefined; |
| 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 _pivotTableHelper?: PivotTableHelperStore = 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 _hasFtrace?: boolean = undefined; |
| private _jobStatus?: Map<ConversionJobName, ConversionJobStatus> = undefined; |
| private _router?: Router = 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 Float64Array(0), |
| utids: new Float64Array(0), |
| trackIds: [], |
| sources: [], |
| totalResults: 0, |
| }; |
| searchSummary: SearchSummary = { |
| tsStarts: new Float64Array(0), |
| tsEnds: new Float64Array(0), |
| count: new Uint8Array(0), |
| }; |
| |
| engines = new Map<string, Engine>(); |
| |
| initialize(dispatch: Dispatch, router: Router) { |
| this._dispatch = dispatch; |
| this._router = router; |
| this._state = createEmptyState(); |
| this._frontendLocalState = new FrontendLocalState(); |
| this._rafScheduler = new RafScheduler(); |
| 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._pivotTableHelper = new Map<string, PivotTableHelper>(); |
| 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(); |
| } |
| |
| 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._state); |
| } |
| |
| set state(state: State) { |
| this._state = assertExists(state); |
| } |
| |
| get dispatch(): Dispatch { |
| return assertExists(this._dispatch); |
| } |
| |
| 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 pivotTableHelper(): PivotTableHelperStore { |
| return assertExists(this._pivotTableHelper); |
| } |
| |
| 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 !!this._hasFtrace; |
| } |
| |
| set hasFtrace(value: boolean) { |
| this._hasFtrace = 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; |
| } |
| |
| 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() { |
| // 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 pxToSec = this.frontendLocalState.timeScale.deltaPxToDuration(1); |
| // TODO(b/186265930): Remove once fixed: |
| if (!isFinite(pxToSec)) { |
| // Resolution is in pixels per second so 1000 means 1px = 1ms. |
| console.error(`b/186265930: Bad pxToSec suppressed ${pxToSec}`); |
| return fromNs(Math.pow(2, Math.floor(Math.log2(toNs(1000))))); |
| } |
| const pxToNs = Math.max(toNs(pxToSec), 1); |
| const resolution = fromNs(Math.pow(2, Math.floor(Math.log2(pxToNs)))); |
| const log2 = Math.log2(toNs(resolution)); |
| if (log2 % 1 !== 0) { |
| throw new Error(`Resolution should be a power of two. |
| pxToSec: ${pxToSec}, |
| pxToNs: ${pxToNs}, |
| resolution: ${resolution}, |
| log2: ${Math.log2(toNs(resolution))}`); |
| } |
| return resolution; |
| } |
| |
| makeSelection(action: DeferredAction<{}>, tabToOpen = 'current_selection') { |
| // A new selection should cancel the current search selection. |
| globals.dispatch(Actions.setSearchIndex({index: -1})); |
| const tab = action.type === 'deselect' ? undefined : tabToOpen; |
| globals.dispatch(Actions.setCurrentTab({tab})); |
| globals.dispatch(action); |
| } |
| |
| resetForTesting() { |
| this._dispatch = undefined; |
| this._state = undefined; |
| this._frontendLocalState = undefined; |
| this._rafScheduler = undefined; |
| this._serviceWorkerController = undefined; |
| |
| // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads. |
| this._trackDataStore = undefined; |
| this._queryResults = undefined; |
| this._pivotTableHelper = 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 Float64Array(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(); |
| } |
| } |
| |
| export const globals = new Globals(); |