1// Copyright (C) 2018 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} from '../base/logging';
16import {DeferredAction} from '../common/actions';
17import {AggregateData} from '../common/aggregation_data';
18import {Args, ArgsTree} from '../common/arg_types';
19import {MetricResult} from '../common/metric_data';
20import {CurrentSearchResults, SearchSummary} from '../common/search_data';
21import {CallsiteInfo, createEmptyState, State} from '../common/state';
22import {fromNs, toNs} from '../common/time';
23import {Analytics, initAnalytics} from '../frontend/analytics';
24
25import {FrontendLocalState} from './frontend_local_state';
26import {RafScheduler} from './raf_scheduler';
27import {ServiceWorkerController} from './service_worker_controller';
28
29type Dispatch = (action: DeferredAction) => void;
30type TrackDataStore = Map<string, {}>;
31type QueryResultsStore = Map<string, {}>;
32type AggregateDataStore = Map<string, AggregateData>;
33type Description = Map<string, string>;
34export interface SliceDetails {
35  ts?: number;
36  dur?: number;
37  priority?: number;
38  endState?: string;
39  cpu?: number;
40  id?: number;
41  threadStateId?: number;
42  utid?: number;
43  wakeupTs?: number;
44  wakerUtid?: number;
45  wakerCpu?: number;
46  category?: string;
47  name?: string;
48  args?: Args;
49  argsTree?: ArgsTree;
50  description?: Description;
51}
52
53export interface FlowPoint {
54  trackId: number;
55
56  sliceName: string;
57  sliceCategory: string;
58  sliceId: number;
59  sliceStartTs: number;
60  sliceEndTs: number;
61
62  depth: number;
63}
64
65export interface Flow {
66  id: number;
67
68  begin: FlowPoint;
69  end: FlowPoint;
70
71  category?: string;
72  name?: string;
73}
74
75export interface CounterDetails {
76  startTime?: number;
77  value?: number;
78  delta?: number;
79  duration?: number;
80}
81
82export interface ThreadStateDetails {
83  ts?: number;
84  dur?: number;
85  state?: string;
86  utid?: number;
87  cpu?: number;
88  sliceId?: number;
89  blockedFunction?: string;
90}
91
92export interface HeapProfileDetails {
93  type?: string;
94  id?: number;
95  ts?: number;
96  tsNs?: number;
97  pid?: number;
98  upid?: number;
99  flamegraph?: CallsiteInfo[];
100  expandedCallsite?: CallsiteInfo;
101  viewingOption?: string;
102  expandedId?: number;
103}
104
105export interface CpuProfileDetails {
106  id?: number;
107  ts?: number;
108  utid?: number;
109  stack?: CallsiteInfo[];
110}
111
112export interface QuantizedLoad {
113  startSec: number;
114  endSec: number;
115  load: number;
116}
117type OverviewStore = Map<string, QuantizedLoad[]>;
118
119export interface ThreadDesc {
120  utid: number;
121  tid: number;
122  threadName: string;
123  pid?: number;
124  procName?: string;
125  cmdline?: string;
126}
127type ThreadMap = Map<number, ThreadDesc>;
128
129function getRoot() {
130  // Works out the root directory where the content should be served from
131  // e.g. `http://origin/v1.2.3/`.
132  let root = (document.currentScript as HTMLScriptElement).src;
133  root = root.substr(0, root.lastIndexOf('/') + 1);
134  return root;
135}
136
137/**
138 * Global accessors for state/dispatch in the frontend.
139 */
140class Globals {
141  readonly root = getRoot();
142
143  private _dispatch?: Dispatch = undefined;
144  private _controllerWorker?: Worker = undefined;
145  private _state?: State = undefined;
146  private _frontendLocalState?: FrontendLocalState = undefined;
147  private _rafScheduler?: RafScheduler = undefined;
148  private _serviceWorkerController?: ServiceWorkerController = undefined;
149  private _logging?: Analytics = undefined;
150  private _isInternalUser: boolean|undefined = undefined;
151  private _channel: string|undefined = undefined;
152
153  // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
154  private _trackDataStore?: TrackDataStore = undefined;
155  private _queryResults?: QueryResultsStore = undefined;
156  private _overviewStore?: OverviewStore = undefined;
157  private _aggregateDataStore?: AggregateDataStore = undefined;
158  private _threadMap?: ThreadMap = undefined;
159  private _sliceDetails?: SliceDetails = undefined;
160  private _threadStateDetails?: ThreadStateDetails = undefined;
161  private _connectedFlows?: Flow[] = undefined;
162  private _selectedFlows?: Flow[] = undefined;
163  private _visibleFlowCategories?: Map<string, boolean> = undefined;
164  private _counterDetails?: CounterDetails = undefined;
165  private _heapProfileDetails?: HeapProfileDetails = undefined;
166  private _cpuProfileDetails?: CpuProfileDetails = undefined;
167  private _numQueriesQueued = 0;
168  private _bufferUsage?: number = undefined;
169  private _recordingLog?: string = undefined;
170  private _traceErrors?: number = undefined;
171  private _metricError?: string = undefined;
172  private _metricResult?: MetricResult = undefined;
173
174  private _currentSearchResults: CurrentSearchResults = {
175    sliceIds: [],
176    tsStarts: [],
177    utids: [],
178    trackIds: [],
179    sources: [],
180    totalResults: 0,
181  };
182  searchSummary: SearchSummary = {
183    tsStarts: new Float64Array(0),
184    tsEnds: new Float64Array(0),
185    count: new Uint8Array(0),
186  };
187
188  initialize(dispatch: Dispatch, controllerWorker: Worker) {
189    this._dispatch = dispatch;
190    this._controllerWorker = controllerWorker;
191    this._state = createEmptyState();
192    this._frontendLocalState = new FrontendLocalState();
193    this._rafScheduler = new RafScheduler();
194    this._serviceWorkerController = new ServiceWorkerController();
195    this._logging = initAnalytics();
196
197    // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
198    this._trackDataStore = new Map<string, {}>();
199    this._queryResults = new Map<string, {}>();
200    this._overviewStore = new Map<string, QuantizedLoad[]>();
201    this._aggregateDataStore = new Map<string, AggregateData>();
202    this._threadMap = new Map<number, ThreadDesc>();
203    this._sliceDetails = {};
204    this._connectedFlows = [];
205    this._selectedFlows = [];
206    this._visibleFlowCategories = new Map<string, boolean>();
207    this._counterDetails = {};
208    this._threadStateDetails = {};
209    this._heapProfileDetails = {};
210    this._cpuProfileDetails = {};
211  }
212
213  get state(): State {
214    return assertExists(this._state);
215  }
216
217  set state(state: State) {
218    this._state = assertExists(state);
219  }
220
221  get dispatch(): Dispatch {
222    return assertExists(this._dispatch);
223  }
224
225  get frontendLocalState() {
226    return assertExists(this._frontendLocalState);
227  }
228
229  get rafScheduler() {
230    return assertExists(this._rafScheduler);
231  }
232
233  get logging() {
234    return assertExists(this._logging);
235  }
236
237  get serviceWorkerController() {
238    return assertExists(this._serviceWorkerController);
239  }
240
241  // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
242  get overviewStore(): OverviewStore {
243    return assertExists(this._overviewStore);
244  }
245
246  get trackDataStore(): TrackDataStore {
247    return assertExists(this._trackDataStore);
248  }
249
250  get queryResults(): QueryResultsStore {
251    return assertExists(this._queryResults);
252  }
253
254  get threads() {
255    return assertExists(this._threadMap);
256  }
257
258  get sliceDetails() {
259    return assertExists(this._sliceDetails);
260  }
261
262  set sliceDetails(click: SliceDetails) {
263    this._sliceDetails = assertExists(click);
264  }
265
266  get threadStateDetails() {
267    return assertExists(this._threadStateDetails);
268  }
269
270  set threadStateDetails(click: ThreadStateDetails) {
271    this._threadStateDetails = assertExists(click);
272  }
273
274  get connectedFlows() {
275    return assertExists(this._connectedFlows);
276  }
277
278  set connectedFlows(connectedFlows: Flow[]) {
279    this._connectedFlows = assertExists(connectedFlows);
280  }
281
282  get selectedFlows() {
283    return assertExists(this._selectedFlows);
284  }
285
286  set selectedFlows(selectedFlows: Flow[]) {
287    this._selectedFlows = assertExists(selectedFlows);
288  }
289
290  get visibleFlowCategories() {
291    return assertExists(this._visibleFlowCategories);
292  }
293
294  set visibleFlowCategories(visibleFlowCategories: Map<string, boolean>) {
295    this._visibleFlowCategories = assertExists(visibleFlowCategories);
296  }
297
298  get counterDetails() {
299    return assertExists(this._counterDetails);
300  }
301
302  set counterDetails(click: CounterDetails) {
303    this._counterDetails = assertExists(click);
304  }
305
306  get aggregateDataStore(): AggregateDataStore {
307    return assertExists(this._aggregateDataStore);
308  }
309
310  get heapProfileDetails() {
311    return assertExists(this._heapProfileDetails);
312  }
313
314  set heapProfileDetails(click: HeapProfileDetails) {
315    this._heapProfileDetails = assertExists(click);
316  }
317
318  get traceErrors() {
319    return this._traceErrors;
320  }
321
322  setTraceErrors(arg: number) {
323    this._traceErrors = arg;
324  }
325
326  get metricError() {
327    return this._metricError;
328  }
329
330  setMetricError(arg: string) {
331    this._metricError = arg;
332  }
333
334  get metricResult() {
335    return this._metricResult;
336  }
337
338  setMetricResult(result: MetricResult) {
339    this._metricResult = result;
340  }
341
342  get cpuProfileDetails() {
343    return assertExists(this._cpuProfileDetails);
344  }
345
346  set cpuProfileDetails(click: CpuProfileDetails) {
347    this._cpuProfileDetails = assertExists(click);
348  }
349
350  set numQueuedQueries(value: number) {
351    this._numQueriesQueued = value;
352  }
353
354  get numQueuedQueries() {
355    return this._numQueriesQueued;
356  }
357
358  get bufferUsage() {
359    return this._bufferUsage;
360  }
361
362  get recordingLog() {
363    return this._recordingLog;
364  }
365
366  get currentSearchResults() {
367    return this._currentSearchResults;
368  }
369
370  set currentSearchResults(results: CurrentSearchResults) {
371    this._currentSearchResults = results;
372  }
373
374  setBufferUsage(bufferUsage: number) {
375    this._bufferUsage = bufferUsage;
376  }
377
378  setTrackData(id: string, data: {}) {
379    this.trackDataStore.set(id, data);
380  }
381
382  setRecordingLog(recordingLog: string) {
383    this._recordingLog = recordingLog;
384  }
385
386  setAggregateData(kind: string, data: AggregateData) {
387    this.aggregateDataStore.set(kind, data);
388  }
389
390  getCurResolution() {
391    // Truncate the resolution to the closest power of 2 (in nanosecond space).
392    // We choose to work in ns space because resolution is consumed be track
393    // controllers for quantization and they rely on resolution to be a power
394    // of 2 in nanosecond form. This is property does not hold if we work in
395    // second space.
396    //
397    // This effectively means the resolution changes approximately every 6 zoom
398    // levels. Logic: each zoom level represents a delta of 0.1 * (visible
399    // window span). Therefore, zooming out by six levels is 1.1^6 ~= 2.
400    // Similarily, zooming in six levels is 0.9^6 ~= 0.5.
401    const pxToSec = this.frontendLocalState.timeScale.deltaPxToDuration(1);
402    // TODO(b/186265930): Remove once fixed:
403    if (!isFinite(pxToSec)) {
404      // Resolution is in pixels per second so 1000 means 1px = 1ms.
405      console.error(`b/186265930: Bad pxToSec suppressed ${pxToSec}`);
406      return fromNs(Math.pow(2, Math.floor(Math.log2(toNs(1000)))));
407    }
408    const pxToNs = Math.max(toNs(pxToSec), 1);
409    const resolution = fromNs(Math.pow(2, Math.floor(Math.log2(pxToNs))));
410    const log2 = Math.log2(toNs(resolution));
411    if (log2 % 1 !== 0) {
412      throw new Error(`Resolution should be a power of two.
413        pxToSec: ${pxToSec},
414        pxToNs: ${pxToNs},
415        resolution: ${resolution},
416        log2: ${Math.log2(toNs(resolution))}`);
417    }
418    return resolution;
419  }
420
421  makeSelection(action: DeferredAction<{}>, tabToOpen = 'current_selection') {
422    // A new selection should cancel the current search selection.
423    globals.frontendLocalState.searchIndex = -1;
424    globals.frontendLocalState.currentTab =
425        action.type === 'deselect' ? undefined : tabToOpen;
426    globals.dispatch(action);
427  }
428
429  resetForTesting() {
430    this._dispatch = undefined;
431    this._state = undefined;
432    this._frontendLocalState = undefined;
433    this._rafScheduler = undefined;
434    this._serviceWorkerController = undefined;
435
436    // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
437    this._trackDataStore = undefined;
438    this._queryResults = undefined;
439    this._overviewStore = undefined;
440    this._threadMap = undefined;
441    this._sliceDetails = undefined;
442    this._threadStateDetails = undefined;
443    this._aggregateDataStore = undefined;
444    this._numQueriesQueued = 0;
445    this._metricResult = undefined;
446    this._currentSearchResults = {
447      sliceIds: [],
448      tsStarts: [],
449      utids: [],
450      trackIds: [],
451      sources: [],
452      totalResults: 0,
453    };
454  }
455
456  // This variable is set by the is_internal_user.js script if the user is a
457  // googler. This is used to avoid exposing features that are not ready yet
458  // for public consumption. The gated features themselves are not secret.
459  // If a user has been detected as a Googler once, make that sticky in
460  // localStorage, so that we keep treating them as such when they connect over
461  // public networks.
462  get isInternalUser() {
463    if (this._isInternalUser === undefined) {
464      this._isInternalUser = localStorage.getItem('isInternalUser') === '1';
465    }
466    return this._isInternalUser;
467  }
468
469  set isInternalUser(value: boolean) {
470    localStorage.setItem('isInternalUser', value ? '1' : '0');
471    this._isInternalUser = value;
472  }
473
474  get channel() {
475    if (this._channel === undefined) {
476      this._channel = localStorage.getItem('perfettoUiChannel') || 'stable';
477    }
478    return this._channel;
479  }
480
481  // Used when switching to the legacy TraceViewer UI.
482  // Most resources are cleaned up by replacing the current |window| object,
483  // however pending RAFs and workers seem to outlive the |window| and need to
484  // be cleaned up explicitly.
485  shutdown() {
486    this._controllerWorker!.terminate();
487    this._rafScheduler!.shutdown();
488  }
489}
490
491export const globals = new Globals();
492