• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
15// Need to turn off Long
16import '../common/query_result';
17
18import {Patch, produce} from 'immer';
19import m from 'mithril';
20
21import {defer} from '../base/deferred';
22import {assertExists, reportError, setErrorHandler} from '../base/logging';
23import {Actions, DeferredAction, StateActions} from '../common/actions';
24import {createEmptyState} from '../common/empty_state';
25import {RECORDING_V2_FLAG} from '../common/feature_flags';
26import {initializeImmerJs} from '../common/immer_init';
27import {pluginManager, pluginRegistry} from '../common/plugins';
28import {onSelectionChanged} from '../common/selection_observer';
29import {State} from '../common/state';
30import {initWasm} from '../common/wasm_engine_proxy';
31import {initController, runControllers} from '../controller';
32import {
33  isGetCategoriesResponse,
34} from '../controller/chrome_proxy_record_controller';
35
36import {AnalyzePage} from './analyze_page';
37import {initCssConstants} from './css_constants';
38import {registerDebugGlobals} from './debug';
39import {maybeShowErrorDialog} from './error_dialog';
40import {installFileDropHandler} from './file_drop_handler';
41import {FlagsPage} from './flags_page';
42import {globals} from './globals';
43import {HomePage} from './home_page';
44import {initLiveReloadIfLocalhost} from './live_reload';
45import {MetricsPage} from './metrics_page';
46import {postMessageHandler} from './post_message_handler';
47import {RecordPage, updateAvailableAdbDevices} from './record_page';
48import {RecordPageV2} from './record_page_v2';
49import {Router} from './router';
50import {CheckHttpRpcConnection} from './rpc_http_dialog';
51import {TraceInfoPage} from './trace_info_page';
52import {maybeOpenTraceFromRoute} from './trace_url_handler';
53import {ViewerPage} from './viewer_page';
54import {WidgetsPage} from './widgets_page';
55
56const EXTENSION_ID = 'lfmkphfpdbjijhpomgecfikhfohaoine';
57
58class FrontendApi {
59  private state: State;
60
61  constructor() {
62    this.state = createEmptyState();
63  }
64
65  dispatchMultiple(actions: DeferredAction[]) {
66    const oldState = this.state;
67    const patches: Patch[] = [];
68    for (const action of actions) {
69      const originalLength = patches.length;
70      const morePatches = this.applyAction(action);
71      patches.length += morePatches.length;
72      for (let i = 0; i < morePatches.length; ++i) {
73        patches[i + originalLength] = morePatches[i];
74      }
75    }
76
77    if (this.state === oldState) {
78      return;
79    }
80
81    // Update overall state.
82    globals.state = this.state;
83
84    // If the visible time in the global state has been updated more recently
85    // than the visible time handled by the frontend @ 60fps, update it. This
86    // typically happens when restoring the state from a permalink.
87    globals.frontendLocalState.mergeState(this.state.frontendLocalState);
88
89    // Only redraw if something other than the frontendLocalState changed.
90    let key: keyof State;
91    for (key in this.state) {
92      if (key !== 'frontendLocalState' && key !== 'visibleTracks' &&
93          oldState[key] !== this.state[key]) {
94        globals.rafScheduler.scheduleFullRedraw();
95        break;
96      }
97    }
98
99    if (this.state.currentSelection !== oldState.currentSelection) {
100      // TODO(altimin): Currently we are not triggering this when changing
101      // the set of selected tracks via toggling per-track checkboxes.
102      // Fix that.
103      onSelectionChanged(
104          this.state.currentSelection || undefined,
105          oldState.currentSelection || undefined);
106    }
107
108    if (patches.length > 0) {
109      // Need to avoid reentering the controller so move this to a
110      // separate task.
111      setTimeout(() => {
112        runControllers();
113      }, 0);
114    }
115  }
116
117  private applyAction(action: DeferredAction): Patch[] {
118    const patches: Patch[] = [];
119
120    // 'produce' creates a immer proxy which wraps the current state turning
121    // all imperative mutations of the state done in the callback into
122    // immutable changes to the returned state.
123    this.state = produce(
124        this.state,
125        (draft) => {
126          (StateActions as any)[action.type](draft, action.args);
127        },
128        (morePatches, _) => {
129          const originalLength = patches.length;
130          patches.length += morePatches.length;
131          for (let i = 0; i < morePatches.length; ++i) {
132            patches[i + originalLength] = morePatches[i];
133          }
134        });
135    return patches;
136  }
137}
138
139function setExtensionAvailability(available: boolean) {
140  globals.dispatch(Actions.setExtensionAvailable({
141    available,
142  }));
143}
144
145function initGlobalsFromQueryString() {
146  const queryString = window.location.search;
147  globals.embeddedMode = queryString.includes('mode=embedded');
148  globals.hideSidebar = queryString.includes('hideSidebar=true');
149}
150
151function setupContentSecurityPolicy() {
152  // Note: self and sha-xxx must be quoted, urls data: and blob: must not.
153  const policy = {
154    'default-src': [
155      `'self'`,
156      // Google Tag Manager bootstrap.
157      `'sha256-LirUKeorCU4uRNtNzr8tlB11uy8rzrdmqHCX38JSwHY='`,
158    ],
159    'script-src': [
160      `'self'`,
161      // TODO(b/201596551): this is required for Wasm after crrev.com/c/3179051
162      // and should be replaced with 'wasm-unsafe-eval'.
163      `'unsafe-eval'`,
164      'https://*.google.com',
165      'https://*.googleusercontent.com',
166      'https://www.googletagmanager.com',
167      'https://www.google-analytics.com',
168    ],
169    'object-src': ['none'],
170    'connect-src': [
171      `'self'`,
172      'http://127.0.0.1:9001',  // For trace_processor_shell --httpd.
173      'ws://127.0.0.1:9001',    // Ditto, for the websocket RPC.
174      'ws://127.0.0.1:8037',    // For the adb websocket server.
175      'https://www.google-analytics.com',
176      'https://*.googleapis.com',  // For Google Cloud Storage fetches.
177      'blob:',
178      'data:',
179    ],
180    'img-src': [
181      `'self'`,
182      'data:',
183      'blob:',
184      'https://www.google-analytics.com',
185      'https://www.googletagmanager.com',
186    ],
187    'navigate-to': ['https://*.perfetto.dev', 'self'],
188  };
189  const meta = document.createElement('meta');
190  meta.httpEquiv = 'Content-Security-Policy';
191  let policyStr = '';
192  for (const [key, list] of Object.entries(policy)) {
193    policyStr += `${key} ${list.join(' ')}; `;
194  }
195  meta.content = policyStr;
196  document.head.appendChild(meta);
197}
198
199function main() {
200  setupContentSecurityPolicy();
201
202  // Load the css. The load is asynchronous and the CSS is not ready by the time
203  // appenChild returns.
204  const cssLoadPromise = defer<void>();
205  const css = document.createElement('link');
206  css.rel = 'stylesheet';
207  css.href = globals.root + 'perfetto.css';
208  css.onload = () => cssLoadPromise.resolve();
209  css.onerror = (err) => cssLoadPromise.reject(err);
210  const favicon = document.head.querySelector('#favicon') as HTMLLinkElement;
211  if (favicon) favicon.href = globals.root + 'assets/favicon.png';
212
213  // Load the script to detect if this is a Googler (see comments on globals.ts)
214  // and initialize GA after that (or after a timeout if something goes wrong).
215  const script = document.createElement('script');
216  script.src =
217      'https://storage.cloud.google.com/perfetto-ui-internal/is_internal_user.js';
218  script.async = true;
219  script.onerror = () => globals.logging.initialize();
220  script.onload = () => globals.logging.initialize();
221  setTimeout(() => globals.logging.initialize(), 5000);
222
223  document.head.append(script, css);
224
225  // Add Error handlers for JS error and for uncaught exceptions in promises.
226  setErrorHandler((err: string) => maybeShowErrorDialog(err));
227  window.addEventListener('error', (e) => reportError(e));
228  window.addEventListener('unhandledrejection', (e) => reportError(e));
229
230  const extensionLocalChannel = new MessageChannel();
231
232  initWasm(globals.root);
233  initializeImmerJs();
234  initController(extensionLocalChannel.port1);
235
236  const dispatch = (action: DeferredAction) => {
237    frontendApi.dispatchMultiple([action]);
238  };
239
240  const router = new Router({
241    '/': HomePage,
242    '/viewer': ViewerPage,
243    '/record': RECORDING_V2_FLAG.get() ? RecordPageV2 : RecordPage,
244    '/query': AnalyzePage,
245    '/flags': FlagsPage,
246    '/metrics': MetricsPage,
247    '/info': TraceInfoPage,
248    '/widgets': WidgetsPage,
249  });
250  router.onRouteChanged = (route) => {
251    globals.rafScheduler.scheduleFullRedraw();
252    maybeOpenTraceFromRoute(route);
253  };
254
255  // This must be called before calling `globals.initialize` so that the
256  // `embeddedMode` global is set.
257  initGlobalsFromQueryString();
258
259  globals.initialize(dispatch, router);
260  globals.serviceWorkerController.install();
261
262  const frontendApi = new FrontendApi();
263  globals.publishRedraw = () => globals.rafScheduler.scheduleFullRedraw();
264
265  // We proxy messages between the extension and the controller because the
266  // controller's worker can't access chrome.runtime.
267  const extensionPort = window.chrome && chrome.runtime ?
268      chrome.runtime.connect(EXTENSION_ID) :
269      undefined;
270
271  setExtensionAvailability(extensionPort !== undefined);
272
273  if (extensionPort) {
274    extensionPort.onDisconnect.addListener((_) => {
275      setExtensionAvailability(false);
276      void chrome.runtime.lastError;  // Needed to not receive an error log.
277    });
278    // This forwards the messages from the extension to the controller.
279    extensionPort.onMessage.addListener(
280        (message: object, _port: chrome.runtime.Port) => {
281          if (isGetCategoriesResponse(message)) {
282            globals.dispatch(Actions.setChromeCategories(message));
283            return;
284          }
285          extensionLocalChannel.port2.postMessage(message);
286        });
287  }
288
289  // This forwards the messages from the controller to the extension
290  extensionLocalChannel.port2.onmessage = ({data}) => {
291    if (extensionPort) extensionPort.postMessage(data);
292  };
293
294  // Put debug variables in the global scope for better debugging.
295  registerDebugGlobals();
296
297  // Prevent pinch zoom.
298  document.body.addEventListener('wheel', (e: MouseEvent) => {
299    if (e.ctrlKey) e.preventDefault();
300  }, {passive: false});
301
302  cssLoadPromise.then(() => onCssLoaded());
303
304  if (globals.testing) {
305    document.body.classList.add('testing');
306  }
307
308  // Initialize all plugins:
309  for (const plugin of pluginRegistry.values()) {
310    pluginManager.activatePlugin(plugin.pluginId);
311  }
312}
313
314
315function onCssLoaded() {
316  initCssConstants();
317  // Clear all the contents of the initial page (e.g. the <pre> error message)
318  // And replace it with the root <main> element which will be used by mithril.
319  document.body.innerHTML = '<main></main>';
320  const main = assertExists(document.body.querySelector('main'));
321  globals.rafScheduler.domRedraw = () => {
322    m.render(main, globals.router.resolve());
323  };
324
325  initLiveReloadIfLocalhost();
326
327  if (!RECORDING_V2_FLAG.get()) {
328    updateAvailableAdbDevices();
329    try {
330      navigator.usb.addEventListener(
331          'connect', () => updateAvailableAdbDevices());
332      navigator.usb.addEventListener(
333          'disconnect', () => updateAvailableAdbDevices());
334    } catch (e) {
335      console.error('WebUSB API not supported');
336    }
337  }
338
339  // Will update the chip on the sidebar footer that notifies that the RPC is
340  // connected. Has no effect on the controller (which will repeat this check
341  // before creating a new engine).
342  // Don't auto-open any trace URLs until we get a response here because we may
343  // accidentially clober the state of an open trace processor instance
344  // otherwise.
345  CheckHttpRpcConnection().then(() => {
346    if (!globals.embeddedMode) {
347      installFileDropHandler();
348    }
349
350    // Don't allow postMessage or opening trace from route when the user says
351    // that they want to reuse the already loaded trace in trace processor.
352    const engine = globals.getCurrentEngine();
353    if (engine && engine.source.type === 'HTTP_RPC') {
354      return;
355    }
356
357    // Add support for opening traces from postMessage().
358    window.addEventListener('message', postMessageHandler, {passive: true});
359
360    // Handles the initial ?local_cache_key=123 or ?s=permalink or ?url=...
361    // cases.
362    maybeOpenTraceFromRoute(Router.parseUrl(window.location.href));
363  });
364}
365
366main();
367