| // 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 MicroModal from 'micromodal'; |
| import * as m from 'mithril'; |
| |
| 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 {maybeShowErrorDialog} from './error_dialog'; |
| import { |
| CounterDetails, |
| CpuProfileDetails, |
| Flow, |
| globals, |
| HeapProfileDetails, |
| QuantizedLoad, |
| SliceDetails, |
| ThreadDesc, |
| ThreadStateDetails |
| } from './globals'; |
| import {HomePage} from './home_page'; |
| import {openBufferWithLegacyTraceViewer} from './legacy_trace_viewer'; |
| 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'; |
| |
| /** |
| * 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); |
| } |
| |
| 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 main() { |
| // 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('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); |
| }); |
| } |
| |
| updateAvailableAdbDevices(); |
| try { |
| navigator.usb.addEventListener( |
| 'connect', () => updateAvailableAdbDevices()); |
| navigator.usb.addEventListener( |
| 'disconnect', () => updateAvailableAdbDevices()); |
| } catch (e) { |
| console.error('WebUSB API not supported'); |
| } |
| // This forwards the messages from the controller to the extension |
| extensionLocalChannel.port2.onmessage = ({data}) => { |
| if (extensionPort) extensionPort.postMessage(data); |
| }; |
| const main = assertExists(document.body.querySelector('main')); |
| |
| globals.rafScheduler.domRedraw = () => |
| m.render(main, m(router.resolve(globals.state.route))); |
| |
| // Add support for opening traces from postMessage(). |
| window.addEventListener('message', postMessageHandler, {passive: true}); |
| |
| // 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; |
| |
| // /?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) { |
| 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); |
| }); |
| } |
| |
| // Prevent pinch zoom. |
| document.body.addEventListener('wheel', (e: MouseEvent) => { |
| if (e.ctrlKey) e.preventDefault(); |
| }, {passive: false}); |
| |
| router.navigateToCurrentHash(); |
| |
| MicroModal.init(); |
| |
| // 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(); |
| } |
| |
| main(); |