|  | // Copyright (C) 2018 The Android Open Source Project | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | import '../tracks/all_frontend'; | 
|  |  | 
|  | import {applyPatches, Patch} from 'immer'; | 
|  | import * as m from 'mithril'; | 
|  |  | 
|  | import {defer} from '../base/deferred'; | 
|  | import {assertExists, reportError, setErrorHandler} from '../base/logging'; | 
|  | import {forwardRemoteCalls} from '../base/remote'; | 
|  | import {Actions} from '../common/actions'; | 
|  | import {AggregateData} from '../common/aggregation_data'; | 
|  | import { | 
|  | LogBoundsKey, | 
|  | LogEntriesKey, | 
|  | LogExists, | 
|  | LogExistsKey | 
|  | } from '../common/logs'; | 
|  | import {MetricResult} from '../common/metric_data'; | 
|  | import {CurrentSearchResults, SearchSummary} from '../common/search_data'; | 
|  |  | 
|  | import {AnalyzePage} from './analyze_page'; | 
|  | import {loadAndroidBugToolInfo} from './android_bug_tool'; | 
|  | import {initCssConstants} from './css_constants'; | 
|  | import {maybeShowErrorDialog} from './error_dialog'; | 
|  | import {installFileDropHandler} from './file_drop_handler'; | 
|  | import { | 
|  | CounterDetails, | 
|  | CpuProfileDetails, | 
|  | Flow, | 
|  | globals, | 
|  | HeapProfileDetails, | 
|  | QuantizedLoad, | 
|  | SliceDetails, | 
|  | ThreadDesc, | 
|  | ThreadStateDetails | 
|  | } from './globals'; | 
|  | import {HomePage} from './home_page'; | 
|  | import {openBufferWithLegacyTraceViewer} from './legacy_trace_viewer'; | 
|  | import {initLiveReloadIfLocalhost} from './live_reload'; | 
|  | import {MetricsPage} from './metrics_page'; | 
|  | import {postMessageHandler} from './post_message_handler'; | 
|  | import {RecordPage, updateAvailableAdbDevices} from './record_page'; | 
|  | import {Router} from './router'; | 
|  | import {CheckHttpRpcConnection} from './rpc_http_dialog'; | 
|  | import {taskTracker} from './task_tracker'; | 
|  | import {TraceInfoPage} from './trace_info_page'; | 
|  | import {ViewerPage} from './viewer_page'; | 
|  |  | 
|  | const EXTENSION_ID = 'lfmkphfpdbjijhpomgecfikhfohaoine'; | 
|  |  | 
|  | function isLocalhostTraceUrl(url: string): boolean { | 
|  | return ['127.0.0.1', 'localhost'].includes((new URL(url)).hostname); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * The API the main thread exposes to the controller. | 
|  | */ | 
|  | class FrontendApi { | 
|  | constructor(private router: Router) {} | 
|  |  | 
|  | patchState(patches: Patch[]) { | 
|  | const oldState = globals.state; | 
|  | globals.state = applyPatches(globals.state, patches); | 
|  |  | 
|  | // If the visible time in the global state has been updated more recently | 
|  | // than the visible time handled by the frontend @ 60fps, update it. This | 
|  | // typically happens when restoring the state from a permalink. | 
|  | globals.frontendLocalState.mergeState(globals.state.frontendLocalState); | 
|  |  | 
|  | // Only redraw if something other than the frontendLocalState changed. | 
|  | for (const key in globals.state) { | 
|  | if (key !== 'frontendLocalState' && key !== 'visibleTracks' && | 
|  | oldState[key] !== globals.state[key]) { | 
|  | this.redraw(); | 
|  | return; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // TODO: we can't have a publish method for each batch of data that we don't | 
|  | // want to keep in the global state. Figure out a more generic and type-safe | 
|  | // mechanism to achieve this. | 
|  |  | 
|  | publishOverviewData(data: {[key: string]: QuantizedLoad|QuantizedLoad[]}) { | 
|  | for (const [key, value] of Object.entries(data)) { | 
|  | if (!globals.overviewStore.has(key)) { | 
|  | globals.overviewStore.set(key, []); | 
|  | } | 
|  | if (value instanceof Array) { | 
|  | globals.overviewStore.get(key)!.push(...value); | 
|  | } else { | 
|  | globals.overviewStore.get(key)!.push(value); | 
|  | } | 
|  | } | 
|  | globals.rafScheduler.scheduleRedraw(); | 
|  | } | 
|  |  | 
|  | publishTrackData(args: {id: string, data: {}}) { | 
|  | globals.setTrackData(args.id, args.data); | 
|  | if ([LogExistsKey, LogBoundsKey, LogEntriesKey].includes(args.id)) { | 
|  | const data = globals.trackDataStore.get(LogExistsKey) as LogExists; | 
|  | if (data && data.exists) globals.rafScheduler.scheduleFullRedraw(); | 
|  | } else { | 
|  | globals.rafScheduler.scheduleRedraw(); | 
|  | } | 
|  | } | 
|  |  | 
|  | publishQueryResult(args: {id: string, data: {}}) { | 
|  | globals.queryResults.set(args.id, args.data); | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishThreads(data: ThreadDesc[]) { | 
|  | globals.threads.clear(); | 
|  | data.forEach(thread => { | 
|  | globals.threads.set(thread.utid, thread); | 
|  | }); | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishSliceDetails(click: SliceDetails) { | 
|  | globals.sliceDetails = click; | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishThreadStateDetails(click: ThreadStateDetails) { | 
|  | globals.threadStateDetails = click; | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishConnectedFlows(connectedFlows: Flow[]) { | 
|  | globals.connectedFlows = connectedFlows; | 
|  | // Call resetFlowFocus() each time connectedFlows is updated to correctly | 
|  | // navigate using hotkeys. | 
|  | this.resetFlowFocus(); | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | // If a chrome slice is selected and we have any flows in connectedFlows | 
|  | // we will find the flows on the right and left of that slice to set a default | 
|  | // focus. In all other cases the focusedFlowId(Left|Right) will be set to -1. | 
|  | resetFlowFocus() { | 
|  | globals.frontendLocalState.focusedFlowIdLeft = -1; | 
|  | globals.frontendLocalState.focusedFlowIdRight = -1; | 
|  | if (globals.state.currentSelection?.kind === 'CHROME_SLICE') { | 
|  | const sliceId = globals.state.currentSelection.id; | 
|  | for (const flow of globals.connectedFlows) { | 
|  | if (flow.begin.sliceId === sliceId) { | 
|  | globals.frontendLocalState.focusedFlowIdRight = flow.id; | 
|  | } | 
|  | if (flow.end.sliceId === sliceId) { | 
|  | globals.frontendLocalState.focusedFlowIdLeft = flow.id; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | publishSelectedFlows(selectedFlows: Flow[]) { | 
|  | globals.selectedFlows = selectedFlows; | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishCounterDetails(click: CounterDetails) { | 
|  | globals.counterDetails = click; | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishHeapProfileDetails(click: HeapProfileDetails) { | 
|  | globals.heapProfileDetails = click; | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishCpuProfileDetails(details: CpuProfileDetails) { | 
|  | globals.cpuProfileDetails = details; | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishFileDownload(args: {file: File, name?: string}) { | 
|  | const url = URL.createObjectURL(args.file); | 
|  | const a = document.createElement('a'); | 
|  | a.href = url; | 
|  | a.download = args.name !== undefined ? args.name : args.file.name; | 
|  | document.body.appendChild(a); | 
|  | a.click(); | 
|  | document.body.removeChild(a); | 
|  | URL.revokeObjectURL(url); | 
|  | } | 
|  |  | 
|  | publishLoading(numQueuedQueries: number) { | 
|  | globals.numQueuedQueries = numQueuedQueries; | 
|  | // TODO(hjd): Clean up loadingAnimation given that this now causes a full | 
|  | // redraw anyways. Also this should probably just go via the global state. | 
|  | globals.rafScheduler.scheduleFullRedraw(); | 
|  | } | 
|  |  | 
|  | // For opening JSON/HTML traces with the legacy catapult viewer. | 
|  | publishLegacyTrace(args: {data: ArrayBuffer, size: number}) { | 
|  | const arr = new Uint8Array(args.data, 0, args.size); | 
|  | const str = (new TextDecoder('utf-8')).decode(arr); | 
|  | openBufferWithLegacyTraceViewer('trace.json', str, 0); | 
|  | globals.dispatch(Actions.clearConversionInProgress({})); | 
|  | } | 
|  |  | 
|  | publishBufferUsage(args: {percentage: number}) { | 
|  | globals.setBufferUsage(args.percentage); | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishSearch(args: SearchSummary) { | 
|  | globals.searchSummary = args; | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishSearchResult(args: CurrentSearchResults) { | 
|  | globals.currentSearchResults = args; | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishRecordingLog(args: {logs: string}) { | 
|  | globals.setRecordingLog(args.logs); | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishTraceErrors(numErrors: number) { | 
|  | globals.setTraceErrors(numErrors); | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishMetricError(error: string) { | 
|  | globals.setMetricError(error); | 
|  | globals.logging.logError(error, false); | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishMetricResult(metricResult: MetricResult) { | 
|  | globals.setMetricResult(metricResult); | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | publishAggregateData(args: {data: AggregateData, kind: string}) { | 
|  | globals.setAggregateData(args.kind, args.data); | 
|  | this.redraw(); | 
|  | } | 
|  |  | 
|  | private redraw(): void { | 
|  | if (globals.state.route && | 
|  | globals.state.route !== this.router.getRouteFromHash()) { | 
|  | this.router.setRouteOnHash(globals.state.route); | 
|  | } | 
|  |  | 
|  | globals.rafScheduler.scheduleFullRedraw(); | 
|  | } | 
|  | } | 
|  |  | 
|  | function setExtensionAvailability(available: boolean) { | 
|  | globals.dispatch(Actions.setExtensionAvailable({ | 
|  | available, | 
|  | })); | 
|  | } | 
|  |  | 
|  | function setupContentSecurityPolicy() { | 
|  | // Note: self and sha-xxx must be quoted, urls data: and blob: must not. | 
|  | const policy = { | 
|  | 'default-src': [ | 
|  | `'self'`, | 
|  | // Google Tag Manager bootstrap. | 
|  | `'sha256-LirUKeorCU4uRNtNzr8tlB11uy8rzrdmqHCX38JSwHY='`, | 
|  | ], | 
|  | 'script-src': [ | 
|  | `'self'`, | 
|  | 'https://*.google.com', | 
|  | 'https://*.googleusercontent.com', | 
|  | 'https://www.googletagmanager.com', | 
|  | 'https://www.google-analytics.com', | 
|  | ], | 
|  | 'object-src': ['none'], | 
|  | 'connect-src': [ | 
|  | `'self'`, | 
|  | 'http://127.0.0.1:9001',  // For trace_processor_shell --httpd. | 
|  | 'https://www.google-analytics.com', | 
|  | 'https://*.googleapis.com',  // For Google Cloud Storage fetches. | 
|  | 'blob:', | 
|  | 'data:', | 
|  | ], | 
|  | 'img-src': [ | 
|  | `'self'`, | 
|  | 'data:', | 
|  | 'blob:', | 
|  | 'https://www.google-analytics.com', | 
|  | 'https://www.googletagmanager.com', | 
|  | ], | 
|  | 'navigate-to': ['https://*.perfetto.dev', 'self'], | 
|  | }; | 
|  | const meta = document.createElement('meta'); | 
|  | meta.httpEquiv = 'Content-Security-Policy'; | 
|  | let policyStr = ''; | 
|  | for (const [key, list] of Object.entries(policy)) { | 
|  | policyStr += `${key} ${list.join(' ')}; `; | 
|  | } | 
|  | meta.content = policyStr; | 
|  | document.head.appendChild(meta); | 
|  | } | 
|  |  | 
|  | function main() { | 
|  | setupContentSecurityPolicy(); | 
|  |  | 
|  | // Load the css. The load is asynchronous and the CSS is not ready by the time | 
|  | // appenChild returns. | 
|  | const cssLoadPromise = defer<void>(); | 
|  | const css = document.createElement('link'); | 
|  | css.rel = 'stylesheet'; | 
|  | css.href = globals.root + 'perfetto.css'; | 
|  | css.onload = () => cssLoadPromise.resolve(); | 
|  | css.onerror = (err) => cssLoadPromise.reject(err); | 
|  | const favicon = document.head.querySelector('#favicon') as HTMLLinkElement; | 
|  | if (favicon) favicon.href = globals.root + 'assets/favicon.png'; | 
|  |  | 
|  | // Load the script to detect if this is a Googler (see comments on globals.ts) | 
|  | // and initialize GA after that (or after a timeout if something goes wrong). | 
|  | const script = document.createElement('script'); | 
|  | script.src = | 
|  | 'https://storage.cloud.google.com/perfetto-ui-internal/is_internal_user.js'; | 
|  | script.async = true; | 
|  | script.onerror = () => globals.logging.initialize(); | 
|  | script.onload = () => globals.logging.initialize(); | 
|  | setTimeout(() => globals.logging.initialize(), 5000); | 
|  |  | 
|  | document.head.append(script, css); | 
|  |  | 
|  | // Add Error handlers for JS error and for uncaught exceptions in promises. | 
|  | setErrorHandler((err: string) => maybeShowErrorDialog(err)); | 
|  | window.addEventListener('error', e => reportError(e)); | 
|  | window.addEventListener('unhandledrejection', e => reportError(e)); | 
|  |  | 
|  | const controller = new Worker(globals.root + 'controller_bundle.js'); | 
|  | const frontendChannel = new MessageChannel(); | 
|  | const controllerChannel = new MessageChannel(); | 
|  | const extensionLocalChannel = new MessageChannel(); | 
|  | const errorReportingChannel = new MessageChannel(); | 
|  |  | 
|  | errorReportingChannel.port2.onmessage = (e) => | 
|  | maybeShowErrorDialog(`${e.data}`); | 
|  |  | 
|  | controller.postMessage( | 
|  | { | 
|  | frontendPort: frontendChannel.port1, | 
|  | controllerPort: controllerChannel.port1, | 
|  | extensionPort: extensionLocalChannel.port1, | 
|  | errorReportingPort: errorReportingChannel.port1, | 
|  | }, | 
|  | [ | 
|  | frontendChannel.port1, | 
|  | controllerChannel.port1, | 
|  | extensionLocalChannel.port1, | 
|  | errorReportingChannel.port1, | 
|  | ]); | 
|  |  | 
|  | const dispatch = | 
|  | controllerChannel.port2.postMessage.bind(controllerChannel.port2); | 
|  | globals.initialize(dispatch, controller); | 
|  | globals.serviceWorkerController.install(); | 
|  |  | 
|  | const router = new Router( | 
|  | '/', | 
|  | { | 
|  | '/': HomePage, | 
|  | '/viewer': ViewerPage, | 
|  | '/record': RecordPage, | 
|  | '/query': AnalyzePage, | 
|  | '/metrics': MetricsPage, | 
|  | '/info': TraceInfoPage, | 
|  | }, | 
|  | dispatch, | 
|  | globals.logging); | 
|  | forwardRemoteCalls(frontendChannel.port2, new FrontendApi(router)); | 
|  |  | 
|  | // We proxy messages between the extension and the controller because the | 
|  | // controller's worker can't access chrome.runtime. | 
|  | const extensionPort = window.chrome && chrome.runtime ? | 
|  | chrome.runtime.connect(EXTENSION_ID) : | 
|  | undefined; | 
|  |  | 
|  | setExtensionAvailability(extensionPort !== undefined); | 
|  |  | 
|  | if (extensionPort) { | 
|  | extensionPort.onDisconnect.addListener(_ => { | 
|  | setExtensionAvailability(false); | 
|  | // tslint:disable-next-line: no-unused-expression | 
|  | void chrome.runtime.lastError;  // Needed to not receive an error log. | 
|  | }); | 
|  | // This forwards the messages from the extension to the controller. | 
|  | extensionPort.onMessage.addListener( | 
|  | (message: object, _port: chrome.runtime.Port) => { | 
|  | extensionLocalChannel.port2.postMessage(message); | 
|  | }); | 
|  | } | 
|  |  | 
|  | // This forwards the messages from the controller to the extension | 
|  | extensionLocalChannel.port2.onmessage = ({data}) => { | 
|  | if (extensionPort) extensionPort.postMessage(data); | 
|  | }; | 
|  |  | 
|  | // Put these variables in the global scope for better debugging. | 
|  | (window as {} as {m: {}}).m = m; | 
|  | (window as {} as {globals: {}}).globals = globals; | 
|  | (window as {} as {Actions: {}}).Actions = Actions; | 
|  |  | 
|  | // Prevent pinch zoom. | 
|  | document.body.addEventListener('wheel', (e: MouseEvent) => { | 
|  | if (e.ctrlKey) e.preventDefault(); | 
|  | }, {passive: false}); | 
|  |  | 
|  | cssLoadPromise.then(() => onCssLoaded(router)); | 
|  | } | 
|  |  | 
|  | function onCssLoaded(router: Router) { | 
|  | initCssConstants(); | 
|  | // Clear all the contents of the initial page (e.g. the <pre> error message) | 
|  | // And replace it with the root <main> element which will be used by mithril. | 
|  | document.body.innerHTML = '<main></main>'; | 
|  | const main = assertExists(document.body.querySelector('main')); | 
|  | globals.rafScheduler.domRedraw = () => | 
|  | m.render(main, m(router.resolve(globals.state.route))); | 
|  |  | 
|  | router.navigateToCurrentHash(); | 
|  |  | 
|  | // /?s=xxxx for permalinks. | 
|  | const stateHash = Router.param('s'); | 
|  | const urlHash = Router.param('url'); | 
|  | const androidBugTool = Router.param('openFromAndroidBugTool'); | 
|  | if (typeof stateHash === 'string' && stateHash) { | 
|  | globals.dispatch(Actions.loadPermalink({ | 
|  | hash: stateHash, | 
|  | })); | 
|  | } else if (typeof urlHash === 'string' && urlHash) { | 
|  | if (isLocalhostTraceUrl(urlHash)) { | 
|  | const fileName = urlHash.split('/').pop() || 'local_trace.pftrace'; | 
|  | const request = fetch(urlHash) | 
|  | .then(response => response.blob()) | 
|  | .then(blob => { | 
|  | globals.dispatch(Actions.openTraceFromFile({ | 
|  | file: new File([blob], fileName), | 
|  | })); | 
|  | }) | 
|  | .catch(e => alert(`Could not load local trace ${e}`)); | 
|  | taskTracker.trackPromise(request, 'Downloading local trace'); | 
|  | } else { | 
|  | globals.dispatch(Actions.openTraceFromUrl({ | 
|  | url: urlHash, | 
|  | })); | 
|  | } | 
|  | } else if (androidBugTool) { | 
|  | // TODO(hjd): Unify updateStatus and TaskTracker | 
|  | globals.dispatch(Actions.updateStatus({ | 
|  | msg: 'Loading trace from ABT extension', | 
|  | timestamp: Date.now() / 1000 | 
|  | })); | 
|  | const loadInfo = loadAndroidBugToolInfo(); | 
|  | taskTracker.trackPromise(loadInfo, 'Loading trace from ABT extension'); | 
|  | loadInfo | 
|  | .then(info => { | 
|  | globals.dispatch(Actions.openTraceFromFile({ | 
|  | file: info.file, | 
|  | })); | 
|  | }) | 
|  | .catch(e => { | 
|  | console.error(e); | 
|  | }); | 
|  | } | 
|  |  | 
|  | // Add support for opening traces from postMessage(). | 
|  | window.addEventListener('message', postMessageHandler, {passive: true}); | 
|  |  | 
|  | // Will update the chip on the sidebar footer that notifies that the RPC is | 
|  | // connected. Has no effect on the controller (which will repeat this check | 
|  | // before creating a new engine). | 
|  | CheckHttpRpcConnection(); | 
|  | initLiveReloadIfLocalhost(); | 
|  |  | 
|  | updateAvailableAdbDevices(); | 
|  | try { | 
|  | navigator.usb.addEventListener( | 
|  | 'connect', () => updateAvailableAdbDevices()); | 
|  | navigator.usb.addEventListener( | 
|  | 'disconnect', () => updateAvailableAdbDevices()); | 
|  | } catch (e) { | 
|  | console.error('WebUSB API not supported'); | 
|  | } | 
|  | installFileDropHandler(); | 
|  | } | 
|  |  | 
|  | main(); |