• 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';
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