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