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