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