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