blob: 93151041ada22cc5195829ed193de707e6cbf9f2 [file] [log] [blame]
// Copyright (C) 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as m from 'mithril';
import {Actions} from '../common/actions';
import {tryGetTrace} from '../common/cache_manager';
import {loadAndroidBugToolInfo} from './android_bug_tool';
import {globals} from './globals';
import {showModal} from './modal';
import {Route, Router} from './router';
import {taskTracker} from './task_tracker';
export function maybeOpenTraceFromRoute(route: Route) {
if (route.args.s) {
// /?s=xxxx for permalinks.
globals.dispatch(Actions.loadPermalink({hash: route.args.s}));
return;
}
if (route.args.url) {
// /?url=https://commondatastorage.googleapis.com/bucket/trace
// This really works only for GCS because the Content Security Policy
// forbids any other url.
loadTraceFromUrl(route.args.url);
return;
}
if (route.args.openFromAndroidBugTool) {
// Handles interaction with the Android Bug Tool extension. See b/163421158.
openTraceFromAndroidBugTool();
return;
}
if (route.args.p && route.page === '/record') {
// Handles backwards compatibility for old URLs (linked from various docs),
// generated before we switched URL scheme. e.g., 'record?p=power' vs
// 'record/power'. See b/191255021#comment2.
Router.navigate(`#!/record/${route.args.p}`);
return;
}
if (route.args.local_cache_key) {
// Handles the case of loading traces from the cache storage.
maybeOpenCachedTrace(route.args.local_cache_key);
return;
}
}
/*
* openCachedTrace(uuid) is called: (1) on startup, from frontend/index.ts; (2)
* every time the fragment changes (from Router.onRouteChange).
* This function must be idempotent (imagine this is called on every frame).
* It must take decision based on the app state, not on URL change events.
* Fragment changes are handled by the union of Router.onHashChange() and this
* function, as follows:
* 1. '' -> URL without a ?local_cache_key=xxx arg:
* - no effect (except redrawing)
* 2. URL without local_cache_key -> URL with local_cache_key:
* - Load cached trace (without prompting any dialog).
* - Show a (graceful) error dialog in the case of cache misses.
* 3. '' -> URL with a ?local_cache_key=xxx arg:
* - Same as case 2.
* 4. URL with local_cache_key=1 -> URL with local_cache_key=2:
* a) If 2 != uuid of the trace currently loaded (globals.state.traceUuid):
* - Ask the user if they intend to switch trace and load 2.
* b) If 2 == uuid of current trace (e.g., after a new trace has loaded):
* - no effect (except redrawing).
* 5. URL with local_cache_key -> URL without local_cache_key:
* - Redirect to ?local_cache_key=1234 where 1234 is the UUID of the previous
* URL (this might or might not match globals.state.traceUuid).
*
* Backward navigation cases:
* 6. URL without local_cache_key <- URL with local_cache_key:
* - Same as case 5.
* 7. URL with local_cache_key=1 <- URL with local_cache_key=2:
* - Same as case 4a: go back to local_cache_key=1 but ask the user to confirm.
* 8. landing page <- URL with local_cache_key:
* - Same as case 5: re-append the local_cache_key.
*/
async function maybeOpenCachedTrace(traceUuid: string) {
if (traceUuid === globals.state.traceUuid) {
// Do nothing, matches the currently loaded trace.
return;
}
if (traceUuid === '') {
// This can happen if we switch from an empty UI state to an invalid UUID
// (e.g. due to a cache miss, below). This can also happen if the user just
// types /#!/viewer?local_cache_key=.
return;
}
// This handles the case when a trace T1 is loaded and then the url is set to
// ?local_cache_key=T2. In that case globals.state.traceUuid remains set to T1
// until T2 has been loaded by the trace processor (can take several seconds).
// This early out prevents to re-trigger the openTraceFromXXX() action if the
// URL changes (e.g. if the user navigates back/fwd) while the new trace is
// being loaded.
for (const eng of Object.values(globals.state.engines)) {
if (eng.source.type === 'ARRAY_BUFFER' && eng.source.uuid === traceUuid) {
return;
}
}
// Fetch the trace from the cache storage. If available load it. If not, show
// a dialog informing the user about the cache miss.
const maybeTrace = await tryGetTrace(traceUuid);
const navigateToOldTraceUuid = () => {
Router.navigate(
`#!/viewer?local_cache_key=${globals.state.traceUuid || ''}`);
};
if (!maybeTrace) {
showModal({
title: 'Could not find the trace in the cache storage',
content: m(
'div',
m('p',
'You are trying to load a cached trace by setting the ' +
'?local_cache_key argument in the URL.'),
m('p', 'Unfortunately the trace wasn\'t in the cache storage.'),
m('p',
'This can happen if a tab was discarded and wasn\'t opened ' +
'for too long, or if you just mis-pasted the URL.'),
m('pre', `Trace UUID: ${traceUuid}`),
),
});
navigateToOldTraceUuid();
return;
}
// If the UI is in a blank state (no trace has been ever opened), just load
// the trace without showing any further dialog. This is the case of tab
// discarding, reloading or pasting a url with a local_cache_key in an empty
// instance.
if (globals.state.traceUuid === undefined) {
globals.dispatch(Actions.openTraceFromBuffer(maybeTrace));
return;
}
// If, instead, another trace is loaded, ask confirmation to the user.
// Switching to another trace clears the UI state. It can be quite annoying to
// lose the UI state by accidentally navigating back too much.
let hasOpenedNewTrace = false;
await showModal({
title: 'You are about to load a different trace and reset the UI state',
content: m(
'div',
m('p',
'You are seeing this because you either pasted a URL with ' +
'a different ?local_cache_key=xxx argument or because you hit ' +
'the history back/fwd button and reached a different trace.'),
m('p',
'If you continue another trace will be loaded and the UI ' +
'state will be cleared.'),
m('pre',
`Old trace: ${globals.state.traceUuid || '<no trace>'}\n` +
`New trace: ${traceUuid}`),
),
buttons: [
{
text: 'Continue',
id: 'trace_id_open', // Used by tests.
primary: true,
action: () => {
hasOpenedNewTrace = true;
globals.dispatch(Actions.openTraceFromBuffer(maybeTrace));
}
},
{text: 'Cancel'},
],
});
if (!hasOpenedNewTrace) {
// We handle this after the modal await rather than in the cancel button
// action so this has effect even if the user clicks Esc or clicks outside
// of the modal dialog and dismisses it.
navigateToOldTraceUuid();
}
}
function loadTraceFromUrl(url: string) {
const isLocalhostTraceUrl =
['127.0.0.1', 'localhost'].includes((new URL(url)).hostname);
if (isLocalhostTraceUrl) {
// This handles the special case of tools/record_android_trace serving the
// traces from a local webserver and killing it immediately after having
// seen the HTTP GET request. In those cases store the trace as a file, so
// when users click on share we don't fail the re-fetch().
const fileName = url.split('/').pop() || 'local_trace.pftrace';
const request = fetch(url)
.then(response => response.blob())
.then(blob => {
globals.dispatch(Actions.openTraceFromFile({
file: new File([blob], fileName),
}));
})
.catch(e => alert(`Could not load local trace ${e}`));
taskTracker.trackPromise(request, 'Downloading local trace');
} else {
globals.dispatch(Actions.openTraceFromUrl({url}));
}
}
function openTraceFromAndroidBugTool() {
// TODO(hjd): Unify updateStatus and TaskTracker
globals.dispatch(Actions.updateStatus(
{msg: 'Loading trace from ABT extension', timestamp: Date.now() / 1000}));
const loadInfo = loadAndroidBugToolInfo();
taskTracker.trackPromise(loadInfo, 'Loading trace from ABT extension');
loadInfo
.then(info => {
globals.dispatch(Actions.openTraceFromFile({
file: info.file,
}));
})
.catch(e => {
console.error(e);
});
}