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