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 '../tracks/all_frontend'; 16 17import {applyPatches, Patch} from 'immer'; 18import * as MicroModal from 'micromodal'; 19import * as m from 'mithril'; 20 21import {assertExists, reportError, setErrorHandler} from '../base/logging'; 22import {forwardRemoteCalls} from '../base/remote'; 23import {Actions} from '../common/actions'; 24import {AggregateData} from '../common/aggregation_data'; 25import { 26 LogBoundsKey, 27 LogEntriesKey, 28 LogExists, 29 LogExistsKey 30} from '../common/logs'; 31import {MetricResult} from '../common/metric_data'; 32import {CurrentSearchResults, SearchSummary} from '../common/search_data'; 33 34import {AnalyzePage} from './analyze_page'; 35import {loadAndroidBugToolInfo} from './android_bug_tool'; 36import {maybeShowErrorDialog} from './error_dialog'; 37import { 38 CounterDetails, 39 CpuProfileDetails, 40 Flow, 41 globals, 42 HeapProfileDetails, 43 QuantizedLoad, 44 SliceDetails, 45 ThreadDesc, 46 ThreadStateDetails 47} from './globals'; 48import {HomePage} from './home_page'; 49import {openBufferWithLegacyTraceViewer} from './legacy_trace_viewer'; 50import {MetricsPage} from './metrics_page'; 51import {postMessageHandler} from './post_message_handler'; 52import {RecordPage, updateAvailableAdbDevices} from './record_page'; 53import {Router} from './router'; 54import {CheckHttpRpcConnection} from './rpc_http_dialog'; 55import {taskTracker} from './task_tracker'; 56import {TraceInfoPage} from './trace_info_page'; 57import {ViewerPage} from './viewer_page'; 58 59const EXTENSION_ID = 'lfmkphfpdbjijhpomgecfikhfohaoine'; 60 61/** 62 * The API the main thread exposes to the controller. 63 */ 64class FrontendApi { 65 constructor(private router: Router) {} 66 67 patchState(patches: Patch[]) { 68 const oldState = globals.state; 69 globals.state = applyPatches(globals.state, patches); 70 71 // If the visible time in the global state has been updated more recently 72 // than the visible time handled by the frontend @ 60fps, update it. This 73 // typically happens when restoring the state from a permalink. 74 globals.frontendLocalState.mergeState(globals.state.frontendLocalState); 75 76 // Only redraw if something other than the frontendLocalState changed. 77 for (const key in globals.state) { 78 if (key !== 'frontendLocalState' && key !== 'visibleTracks' && 79 oldState[key] !== globals.state[key]) { 80 this.redraw(); 81 return; 82 } 83 } 84 } 85 86 // TODO: we can't have a publish method for each batch of data that we don't 87 // want to keep in the global state. Figure out a more generic and type-safe 88 // mechanism to achieve this. 89 90 publishOverviewData(data: {[key: string]: QuantizedLoad|QuantizedLoad[]}) { 91 for (const [key, value] of Object.entries(data)) { 92 if (!globals.overviewStore.has(key)) { 93 globals.overviewStore.set(key, []); 94 } 95 if (value instanceof Array) { 96 globals.overviewStore.get(key)!.push(...value); 97 } else { 98 globals.overviewStore.get(key)!.push(value); 99 } 100 } 101 globals.rafScheduler.scheduleRedraw(); 102 } 103 104 publishTrackData(args: {id: string, data: {}}) { 105 globals.setTrackData(args.id, args.data); 106 if ([LogExistsKey, LogBoundsKey, LogEntriesKey].includes(args.id)) { 107 const data = globals.trackDataStore.get(LogExistsKey) as LogExists; 108 if (data && data.exists) globals.rafScheduler.scheduleFullRedraw(); 109 } else { 110 globals.rafScheduler.scheduleRedraw(); 111 } 112 } 113 114 publishQueryResult(args: {id: string, data: {}}) { 115 globals.queryResults.set(args.id, args.data); 116 this.redraw(); 117 } 118 119 publishThreads(data: ThreadDesc[]) { 120 globals.threads.clear(); 121 data.forEach(thread => { 122 globals.threads.set(thread.utid, thread); 123 }); 124 this.redraw(); 125 } 126 127 publishSliceDetails(click: SliceDetails) { 128 globals.sliceDetails = click; 129 this.redraw(); 130 } 131 132 publishThreadStateDetails(click: ThreadStateDetails) { 133 globals.threadStateDetails = click; 134 this.redraw(); 135 } 136 137 publishConnectedFlows(connectedFlows: Flow[]) { 138 globals.connectedFlows = connectedFlows; 139 // Call resetFlowFocus() each time connectedFlows is updated to correctly 140 // navigate using hotkeys. 141 this.resetFlowFocus(); 142 this.redraw(); 143 } 144 145 // If a chrome slice is selected and we have any flows in connectedFlows 146 // we will find the flows on the right and left of that slice to set a default 147 // focus. In all other cases the focusedFlowId(Left|Right) will be set to -1. 148 resetFlowFocus() { 149 globals.frontendLocalState.focusedFlowIdLeft = -1; 150 globals.frontendLocalState.focusedFlowIdRight = -1; 151 if (globals.state.currentSelection?.kind === 'CHROME_SLICE') { 152 const sliceId = globals.state.currentSelection.id; 153 for (const flow of globals.connectedFlows) { 154 if (flow.begin.sliceId === sliceId) { 155 globals.frontendLocalState.focusedFlowIdRight = flow.id; 156 } 157 if (flow.end.sliceId === sliceId) { 158 globals.frontendLocalState.focusedFlowIdLeft = flow.id; 159 } 160 } 161 } 162 } 163 164 publishSelectedFlows(selectedFlows: Flow[]) { 165 globals.selectedFlows = selectedFlows; 166 this.redraw(); 167 } 168 169 publishCounterDetails(click: CounterDetails) { 170 globals.counterDetails = click; 171 this.redraw(); 172 } 173 174 publishHeapProfileDetails(click: HeapProfileDetails) { 175 globals.heapProfileDetails = click; 176 this.redraw(); 177 } 178 179 publishCpuProfileDetails(details: CpuProfileDetails) { 180 globals.cpuProfileDetails = details; 181 this.redraw(); 182 } 183 184 publishFileDownload(args: {file: File, name?: string}) { 185 const url = URL.createObjectURL(args.file); 186 const a = document.createElement('a'); 187 a.href = url; 188 a.download = args.name !== undefined ? args.name : args.file.name; 189 document.body.appendChild(a); 190 a.click(); 191 document.body.removeChild(a); 192 URL.revokeObjectURL(url); 193 } 194 195 publishLoading(numQueuedQueries: number) { 196 globals.numQueuedQueries = numQueuedQueries; 197 // TODO(hjd): Clean up loadingAnimation given that this now causes a full 198 // redraw anyways. Also this should probably just go via the global state. 199 globals.rafScheduler.scheduleFullRedraw(); 200 } 201 202 // For opening JSON/HTML traces with the legacy catapult viewer. 203 publishLegacyTrace(args: {data: ArrayBuffer, size: number}) { 204 const arr = new Uint8Array(args.data, 0, args.size); 205 const str = (new TextDecoder('utf-8')).decode(arr); 206 openBufferWithLegacyTraceViewer('trace.json', str, 0); 207 } 208 209 publishBufferUsage(args: {percentage: number}) { 210 globals.setBufferUsage(args.percentage); 211 this.redraw(); 212 } 213 214 publishSearch(args: SearchSummary) { 215 globals.searchSummary = args; 216 this.redraw(); 217 } 218 219 publishSearchResult(args: CurrentSearchResults) { 220 globals.currentSearchResults = args; 221 this.redraw(); 222 } 223 224 publishRecordingLog(args: {logs: string}) { 225 globals.setRecordingLog(args.logs); 226 this.redraw(); 227 } 228 229 publishTraceErrors(numErrors: number) { 230 globals.setTraceErrors(numErrors); 231 this.redraw(); 232 } 233 234 publishMetricError(error: string) { 235 globals.setMetricError(error); 236 globals.logging.logError(error, false); 237 this.redraw(); 238 } 239 240 publishMetricResult(metricResult: MetricResult) { 241 globals.setMetricResult(metricResult); 242 this.redraw(); 243 } 244 245 publishAggregateData(args: {data: AggregateData, kind: string}) { 246 globals.setAggregateData(args.kind, args.data); 247 this.redraw(); 248 } 249 250 private redraw(): void { 251 if (globals.state.route && 252 globals.state.route !== this.router.getRouteFromHash()) { 253 this.router.setRouteOnHash(globals.state.route); 254 } 255 256 globals.rafScheduler.scheduleFullRedraw(); 257 } 258} 259 260function setExtensionAvailability(available: boolean) { 261 globals.dispatch(Actions.setExtensionAvailable({ 262 available, 263 })); 264} 265 266function main() { 267 // Add Error handlers for JS error and for uncaught exceptions in promises. 268 setErrorHandler((err: string) => maybeShowErrorDialog(err)); 269 window.addEventListener('error', e => reportError(e)); 270 window.addEventListener('unhandledrejection', e => reportError(e)); 271 272 const controller = new Worker('controller_bundle.js'); 273 const frontendChannel = new MessageChannel(); 274 const controllerChannel = new MessageChannel(); 275 const extensionLocalChannel = new MessageChannel(); 276 const errorReportingChannel = new MessageChannel(); 277 278 errorReportingChannel.port2.onmessage = (e) => 279 maybeShowErrorDialog(`${e.data}`); 280 281 controller.postMessage( 282 { 283 frontendPort: frontendChannel.port1, 284 controllerPort: controllerChannel.port1, 285 extensionPort: extensionLocalChannel.port1, 286 errorReportingPort: errorReportingChannel.port1, 287 }, 288 [ 289 frontendChannel.port1, 290 controllerChannel.port1, 291 extensionLocalChannel.port1, 292 errorReportingChannel.port1, 293 ]); 294 295 const dispatch = 296 controllerChannel.port2.postMessage.bind(controllerChannel.port2); 297 globals.initialize(dispatch, controller); 298 globals.serviceWorkerController.install(); 299 300 const router = new Router( 301 '/', 302 { 303 '/': HomePage, 304 '/viewer': ViewerPage, 305 '/record': RecordPage, 306 '/query': AnalyzePage, 307 '/metrics': MetricsPage, 308 '/info': TraceInfoPage, 309 }, 310 dispatch, 311 globals.logging); 312 forwardRemoteCalls(frontendChannel.port2, new FrontendApi(router)); 313 314 // We proxy messages between the extension and the controller because the 315 // controller's worker can't access chrome.runtime. 316 const extensionPort = window.chrome && chrome.runtime ? 317 chrome.runtime.connect(EXTENSION_ID) : 318 undefined; 319 320 setExtensionAvailability(extensionPort !== undefined); 321 322 if (extensionPort) { 323 extensionPort.onDisconnect.addListener(_ => { 324 setExtensionAvailability(false); 325 // tslint:disable-next-line: no-unused-expression 326 void chrome.runtime.lastError; // Needed to not receive an error log. 327 }); 328 // This forwards the messages from the extension to the controller. 329 extensionPort.onMessage.addListener( 330 (message: object, _port: chrome.runtime.Port) => { 331 extensionLocalChannel.port2.postMessage(message); 332 }); 333 } 334 335 updateAvailableAdbDevices(); 336 try { 337 navigator.usb.addEventListener( 338 'connect', () => updateAvailableAdbDevices()); 339 navigator.usb.addEventListener( 340 'disconnect', () => updateAvailableAdbDevices()); 341 } catch (e) { 342 console.error('WebUSB API not supported'); 343 } 344 // This forwards the messages from the controller to the extension 345 extensionLocalChannel.port2.onmessage = ({data}) => { 346 if (extensionPort) extensionPort.postMessage(data); 347 }; 348 const main = assertExists(document.body.querySelector('main')); 349 350 globals.rafScheduler.domRedraw = () => 351 m.render(main, m(router.resolve(globals.state.route))); 352 353 // Add support for opening traces from postMessage(). 354 window.addEventListener('message', postMessageHandler, {passive: true}); 355 356 // Put these variables in the global scope for better debugging. 357 (window as {} as {m: {}}).m = m; 358 (window as {} as {globals: {}}).globals = globals; 359 (window as {} as {Actions: {}}).Actions = Actions; 360 361 // /?s=xxxx for permalinks. 362 const stateHash = Router.param('s'); 363 const urlHash = Router.param('url'); 364 const androidBugTool = Router.param('openFromAndroidBugTool'); 365 if (typeof stateHash === 'string' && stateHash) { 366 globals.dispatch(Actions.loadPermalink({ 367 hash: stateHash, 368 })); 369 } else if (typeof urlHash === 'string' && urlHash) { 370 globals.dispatch(Actions.openTraceFromUrl({ 371 url: urlHash, 372 })); 373 } else if (androidBugTool) { 374 // TODO(hjd): Unify updateStatus and TaskTracker 375 globals.dispatch(Actions.updateStatus({ 376 msg: 'Loading trace from ABT extension', 377 timestamp: Date.now() / 1000 378 })); 379 const loadInfo = loadAndroidBugToolInfo(); 380 taskTracker.trackPromise(loadInfo, 'Loading trace from ABT extension'); 381 loadInfo 382 .then(info => { 383 globals.dispatch(Actions.openTraceFromFile({ 384 file: info.file, 385 })); 386 }) 387 .catch(e => { 388 console.error(e); 389 }); 390 } 391 392 // Prevent pinch zoom. 393 document.body.addEventListener('wheel', (e: MouseEvent) => { 394 if (e.ctrlKey) e.preventDefault(); 395 }, {passive: false}); 396 397 router.navigateToCurrentHash(); 398 399 MicroModal.init(); 400 401 // Will update the chip on the sidebar footer that notifies that the RPC is 402 // connected. Has no effect on the controller (which will repeat this check 403 // before creating a new engine). 404 CheckHttpRpcConnection(); 405} 406 407main(); 408