1// Copyright (C) 2021 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 m from 'mithril'; 16import {tryGetTrace} from '../core/cache_manager'; 17import {showModal} from '../widgets/modal'; 18import {loadPermalink} from './permalink'; 19import {loadAndroidBugToolInfo} from './android_bug_tool'; 20import {Route, Router} from '../core/router'; 21import {taskTracker} from './task_tracker'; 22import {AppImpl} from '../core/app_impl'; 23 24function getCurrentTraceUrl(): undefined | string { 25 const source = AppImpl.instance.trace?.traceInfo.source; 26 if (source && source.type === 'URL') { 27 return source.url; 28 } 29 return undefined; 30} 31 32export function maybeOpenTraceFromRoute(route: Route) { 33 if (route.args.s) { 34 // /?s=xxxx for permalinks. 35 loadPermalink(route.args.s); 36 return; 37 } 38 39 const url = route.args.url; 40 if (url && url !== getCurrentTraceUrl()) { 41 // /?url=https://commondatastorage.googleapis.com/bucket/trace 42 // This really works only for GCS because the Content Security Policy 43 // forbids any other url. 44 loadTraceFromUrl(url); 45 return; 46 } 47 48 if (route.args.openFromAndroidBugTool) { 49 // Handles interaction with the Android Bug Tool extension. See b/163421158. 50 openTraceFromAndroidBugTool(); 51 return; 52 } 53 54 if (route.args.p && route.page === '/record') { 55 // Handles backwards compatibility for old URLs (linked from various docs), 56 // generated before we switched URL scheme. e.g., 'record?p=power' vs 57 // 'record/power'. See b/191255021#comment2. 58 Router.navigate(`#!/record/${route.args.p}`); 59 return; 60 } 61 62 if (route.args.local_cache_key) { 63 // Handles the case of loading traces from the cache storage. 64 maybeOpenCachedTrace(route.args.local_cache_key); 65 return; 66 } 67} 68 69/* 70 * openCachedTrace(uuid) is called: (1) on startup, from frontend/index.ts; (2) 71 * every time the fragment changes (from Router.onRouteChange). 72 * This function must be idempotent (imagine this is called on every frame). 73 * It must take decision based on the app state, not on URL change events. 74 * Fragment changes are handled by the union of Router.onHashChange() and this 75 * function, as follows: 76 * 1. '' -> URL without a ?local_cache_key=xxx arg: 77 * - no effect (except redrawing) 78 * 2. URL without local_cache_key -> URL with local_cache_key: 79 * - Load cached trace (without prompting any dialog). 80 * - Show a (graceful) error dialog in the case of cache misses. 81 * 3. '' -> URL with a ?local_cache_key=xxx arg: 82 * - Same as case 2. 83 * 4. URL with local_cache_key=1 -> URL with local_cache_key=2: 84 * a) If 2 != uuid of the trace currently loaded (TraceImpl.traceInfo.uuid): 85 * - Ask the user if they intend to switch trace and load 2. 86 * b) If 2 == uuid of current trace (e.g., after a new trace has loaded): 87 * - no effect (except redrawing). 88 * 5. URL with local_cache_key -> URL without local_cache_key: 89 * - Redirect to ?local_cache_key=1234 where 1234 is the UUID of the previous 90 * URL (this might or might not match traceInfo.uuid). 91 * 92 * Backward navigation cases: 93 * 6. URL without local_cache_key <- URL with local_cache_key: 94 * - Same as case 5. 95 * 7. URL with local_cache_key=1 <- URL with local_cache_key=2: 96 * - Same as case 4a: go back to local_cache_key=1 but ask the user to confirm. 97 * 8. landing page <- URL with local_cache_key: 98 * - Same as case 5: re-append the local_cache_key. 99 */ 100async function maybeOpenCachedTrace(traceUuid: string) { 101 const curTrace = AppImpl.instance.trace?.traceInfo; 102 const curCacheUuid = curTrace?.cached ? curTrace.uuid : ''; 103 104 if (traceUuid === curCacheUuid) { 105 // Do nothing, matches the currently loaded trace. 106 return; 107 } 108 109 if (traceUuid === '') { 110 // This can happen if we switch from an empty UI state to an invalid UUID 111 // (e.g. due to a cache miss, below). This can also happen if the user just 112 // types /#!/viewer?local_cache_key=. 113 return; 114 } 115 116 // This handles the case when a trace T1 is loaded and then the url is set to 117 // ?local_cache_key=T2. In that case globals.state.traceUuid remains set to T1 118 // until T2 has been loaded by the trace processor (can take several seconds). 119 // This early out prevents to re-trigger the openTraceFromXXX() action if the 120 // URL changes (e.g. if the user navigates back/fwd) while the new trace is 121 // being loaded. 122 if ( 123 curTrace !== undefined && 124 curTrace.source.type === 'ARRAY_BUFFER' && 125 curTrace.source.uuid === traceUuid 126 ) { 127 return; 128 } 129 130 // Fetch the trace from the cache storage. If available load it. If not, show 131 // a dialog informing the user about the cache miss. 132 const maybeTrace = await tryGetTrace(traceUuid); 133 134 const navigateToOldTraceUuid = () => 135 Router.navigate(`#!/viewer?local_cache_key=${curCacheUuid}`); 136 137 if (!maybeTrace) { 138 showModal({ 139 title: 'Could not find the trace in the cache storage', 140 content: m( 141 'div', 142 m( 143 'p', 144 'You are trying to load a cached trace by setting the ' + 145 '?local_cache_key argument in the URL.', 146 ), 147 m('p', "Unfortunately the trace wasn't in the cache storage."), 148 m( 149 'p', 150 "This can happen if a tab was discarded and wasn't opened " + 151 'for too long, or if you just mis-pasted the URL.', 152 ), 153 m('pre', `Trace UUID: ${traceUuid}`), 154 ), 155 }); 156 navigateToOldTraceUuid(); 157 return; 158 } 159 160 // If the UI is in a blank state (no trace has been ever opened), just load 161 // the trace without showing any further dialog. This is the case of tab 162 // discarding, reloading or pasting a url with a local_cache_key in an empty 163 // instance. 164 if (curTrace === undefined) { 165 AppImpl.instance.openTraceFromBuffer(maybeTrace); 166 return; 167 } 168 169 // If, instead, another trace is loaded, ask confirmation to the user. 170 // Switching to another trace clears the UI state. It can be quite annoying to 171 // lose the UI state by accidentally navigating back too much. 172 let hasOpenedNewTrace = false; 173 174 await showModal({ 175 title: 'You are about to load a different trace and reset the UI state', 176 content: m( 177 'div', 178 m( 179 'p', 180 'You are seeing this because you either pasted a URL with ' + 181 'a different ?local_cache_key=xxx argument or because you hit ' + 182 'the history back/fwd button and reached a different trace.', 183 ), 184 m( 185 'p', 186 'If you continue another trace will be loaded and the UI ' + 187 'state will be cleared.', 188 ), 189 m( 190 'pre', 191 `Old trace: ${curTrace !== undefined ? curCacheUuid : '<no trace>'}\n` + 192 `New trace: ${traceUuid}`, 193 ), 194 ), 195 buttons: [ 196 { 197 text: 'Continue', 198 id: 'trace_id_open', // Used by tests. 199 primary: true, 200 action: () => { 201 hasOpenedNewTrace = true; 202 AppImpl.instance.openTraceFromBuffer(maybeTrace); 203 }, 204 }, 205 {text: 'Cancel'}, 206 ], 207 }); 208 209 if (!hasOpenedNewTrace) { 210 // We handle this after the modal await rather than in the cancel button 211 // action so this has effect even if the user clicks Esc or clicks outside 212 // of the modal dialog and dismisses it. 213 navigateToOldTraceUuid(); 214 } 215} 216 217function loadTraceFromUrl(url: string) { 218 const isLocalhostTraceUrl = ['127.0.0.1', 'localhost'].includes( 219 new URL(url).hostname, 220 ); 221 222 if (isLocalhostTraceUrl) { 223 // This handles the special case of tools/record_android_trace serving the 224 // traces from a local webserver and killing it immediately after having 225 // seen the HTTP GET request. In those cases store the trace as a file, so 226 // when users click on share we don't fail the re-fetch(). 227 const fileName = url.split('/').pop() ?? 'local_trace.pftrace'; 228 const request = fetch(url) 229 .then((response) => response.blob()) 230 .then((b) => AppImpl.instance.openTraceFromFile(new File([b], fileName))) 231 .catch((e) => alert(`Could not load local trace ${e}`)); 232 taskTracker.trackPromise(request, 'Downloading local trace'); 233 } else { 234 AppImpl.instance.openTraceFromUrl(url); 235 } 236} 237 238function openTraceFromAndroidBugTool() { 239 const msg = 'Loading trace from ABT extension'; 240 AppImpl.instance.omnibox.showStatusMessage(msg); 241 const loadInfo = loadAndroidBugToolInfo(); 242 taskTracker.trackPromise(loadInfo, msg); 243 loadInfo 244 .then((info) => AppImpl.instance.openTraceFromFile(info.file)) 245 .catch((e) => console.error(e)); 246} 247