ui: Support deeplinks
Change-Id: Ib5d457a9931fcccfed9a063f15dff0bd852e9783
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 15167eb..7b8f462 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -458,6 +458,15 @@
}
},
+ maybeSetPendingDeeplink(
+ state: StateDraft, args: {ts?: string, dur?: string, tid?: string}) {
+ state.pendingDeeplink = args;
+ },
+
+ clearPendingDeeplink(state: StateDraft, _: {}) {
+ state.pendingDeeplink = undefined;
+ },
+
// TODO(hjd): engine.ready should be a published thing. If it's part
// of the state it interacts badly with permalinks.
setEngineReady(
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index aa28c8e..52855b8 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -104,6 +104,7 @@
// 29. Add ftrace state. <-- Borked, state contains a non-serializable object.
// 30. Convert ftraceFilter.excludedNames from Set<string> to string[].
// 31. Convert all timestamps to bigints.
+// 32. Add pendingDeeplink.
export const STATE_VERSION = 31;
export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
@@ -515,6 +516,12 @@
excludedNames: string[];
}
+export interface PendingDeeplinkState {
+ ts?: string;
+ dur?: string;
+ tid?: string;
+}
+
export interface State {
version: number;
nextId: string;
@@ -609,6 +616,10 @@
// Omnibox info.
omniboxState: OmniboxState;
+
+ // Pending deeplink which will happen when we first finish opening a
+ // trace.
+ pendingDeeplink?: PendingDeeplinkState;
}
export const defaultTraceTime = {
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index f0e2d02..49301e3 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -39,7 +39,12 @@
STR_NULL,
} from '../common/query_result';
import {onSelectionChanged} from '../common/selection_observer';
-import {defaultTraceTime, EngineMode, ProfileType} from '../common/state';
+import {
+ defaultTraceTime,
+ EngineMode,
+ PendingDeeplinkState,
+ ProfileType,
+} from '../common/state';
import {Span} from '../common/time';
import {
TPTime,
@@ -523,6 +528,12 @@
await this.selectPerfSample();
}
+ const pendingDeeplink = globals.state.pendingDeeplink;
+ if (pendingDeeplink !== undefined) {
+ globals.dispatch(Actions.clearPendingDeeplink({}));
+ await this.selectPendingDeeplink(pendingDeeplink);
+ }
+
// If the trace was shared via a permalink, it might already have a
// selection. Emit onSelectionChanged to ensure that the components (like
// current selection details) react to it.
@@ -530,6 +541,8 @@
onSelectionChanged(globals.state.currentSelection, undefined);
}
+ globals.dispatch(Actions.maybeExpandOnlyTrackGroup({}));
+
// Trace Processor doesn't support the reliable range feature for JSON
// traces.
if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG.get()) {
@@ -583,6 +596,52 @@
globals.dispatch(Actions.selectHeapProfile({id: 0, upid, ts, type}));
}
+ private async selectPendingDeeplink(link: PendingDeeplinkState) {
+ const conditions = [];
+ const {ts, dur} = link;
+
+ if (ts !== undefined) {
+ conditions.push(`ts = ${ts}`);
+ }
+ if (dur !== undefined) {
+ conditions.push(`dur = ${dur}`);
+ }
+
+ if (conditions.length === 0) {
+ return;
+ }
+
+ const query = `
+ select
+ id,
+ track_id as traceProcessorTrackId,
+ type
+ from slice
+ where ${conditions.join(' and ')}
+ ;`;
+
+ const result = await assertExists(this.engine).query(query);
+ if (result.numRows() > 0) {
+ const row = result.firstRow({
+ id: NUM,
+ traceProcessorTrackId: NUM,
+ type: STR,
+ });
+
+ const id = row.traceProcessorTrackId;
+ const trackId = globals.state.uiTrackIdByTraceTrackId[id];
+ if (trackId === undefined) {
+ return;
+ }
+ globals.makeSelection(Actions.selectChromeSlice({
+ id: row.id,
+ trackId,
+ table: '',
+ scroll: true,
+ }));
+ }
+ }
+
private async listTracks() {
this.updateStatus('Loading tracks');
const engine = assertExists<Engine>(this.engine);
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index a5f4520..053316f 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -337,6 +337,14 @@
// accidentially clober the state of an open trace processor instance
// otherwise.
CheckHttpRpcConnection().then(() => {
+ const route = Router.parseUrl(window.location.href);
+
+ globals.dispatch(Actions.maybeSetPendingDeeplink({
+ ts: route.args.ts,
+ tid: route.args.tid,
+ dur: route.args.dur,
+ }));
+
if (!globals.embeddedMode) {
installFileDropHandler();
}
@@ -353,7 +361,7 @@
// Handles the initial ?local_cache_key=123 or ?s=permalink or ?url=...
// cases.
- maybeOpenTraceFromRoute(Router.parseUrl(window.location.href));
+ maybeOpenTraceFromRoute(route);
});
}
diff --git a/ui/src/frontend/router.ts b/ui/src/frontend/router.ts
index d407a20..1057d80 100644
--- a/ui/src/frontend/router.ts
+++ b/ui/src/frontend/router.ts
@@ -72,6 +72,11 @@
// Should we hide the sidebar?
hideSidebar: optBool,
+
+ // Deep link support
+ ts: optStr,
+ dur: optStr,
+ tid: optStr,
});
type RouteArgs = ValidatedType<typeof routeArgs>;
@@ -145,15 +150,6 @@
newRoute.args.local_cache_key = oldRoute.args.local_cache_key;
}
- // Javascript sadly distinguishes between foo[bar] === undefined
- // and foo[bar] is not set at all. Here we need the second case to
- // avoid making the URL ugly.
- for (const key of Object.keys(newRoute)) {
- if ((newRoute as any)[key] === undefined) {
- delete (newRoute as any)[key];
- }
- }
-
const args = m.buildQueryString(newRoute.args);
let normalizedFragment = `#!${newRoute.page}${newRoute.subpage}`;
normalizedFragment += args.length > 0 ? '?' + args : '';
@@ -187,9 +183,7 @@
// '#!/record/gpu?local_cache_key=abcd-1234'
// Sample output:
// {page: '/record', subpage: '/gpu', args: {local_cache_key: 'abcd-1234'}}
- static parseFragment(hash: string, optDefaultArgs?: RouteArgs): Route {
- const defaultArgs = optDefaultArgs ?? {};
-
+ static parseFragment(hash: string): Route {
const prefixLength = ROUTE_PREFIX.length;
let route = '';
if (hash.startsWith(ROUTE_PREFIX)) {
@@ -206,11 +200,20 @@
const argsStart = hash.indexOf('?');
const argsStr = argsStart < 0 ? '' : hash.substring(argsStart + 1);
- const fragmentArgs =
+ const rawArgs =
argsStr ? m.parseQueryString(hash.substring(argsStart)) : {};
- const rawArgs = Object.assign({}, defaultArgs, fragmentArgs);
+
const args = runValidator(routeArgs, rawArgs).result;
+ // Javascript sadly distinguishes between foo[bar] === undefined
+ // and foo[bar] is not set at all. Here we need the second case to
+ // avoid making the URL ugly.
+ for (const key of Object.keys(args)) {
+ if ((args as any)[key] === undefined) {
+ delete (args as any)[key];
+ }
+ }
+
return {page, subpage, args};
}
@@ -218,6 +221,16 @@
const query = (new URL(url)).search;
const rawArgs = m.parseQueryString(query);
const args = runValidator(routeArgs, rawArgs).result;
+
+ // Javascript sadly distinguishes between foo[bar] === undefined
+ // and foo[bar] is not set at all. Here we need the second case to
+ // avoid making the URL ugly.
+ for (const key of Object.keys(args)) {
+ if ((args as any)[key] === undefined) {
+ delete (args as any)[key];
+ }
+ }
+
return args;
}
@@ -227,7 +240,9 @@
const hashPos = url.indexOf('#');
const fragment = hashPos < 0 ? '' : url.substring(hashPos);
- const route = Router.parseFragment(fragment, searchArgs);
+ const route = Router.parseFragment(fragment);
+ route.args = Object.assign({}, searchArgs, route.args);
+
return route;
}
diff --git a/ui/src/frontend/trace_url_handler.ts b/ui/src/frontend/trace_url_handler.ts
index 8b398fb..5e329f5 100644
--- a/ui/src/frontend/trace_url_handler.ts
+++ b/ui/src/frontend/trace_url_handler.ts
@@ -23,7 +23,6 @@
import {Route, Router} from './router';
import {taskTracker} from './task_tracker';
-
export function maybeOpenTraceFromRoute(route: Route) {
if (route.args.s) {
// /?s=xxxx for permalinks.