xref: /aosp_15_r20/external/perfetto/ui/src/frontend/trace_url_handler.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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