Thread/process details tab
- Add implementations of "details" tab for threads and processes, which
will allow the user to inspect the relevant details via the UI.
- Switch the slice details panel to use "thread / process ref" widget
instead of just displaying the name, which would allow the user to
inspect the relevant data using the UI.
R=stevegolton@google.com
Change-Id: I493c317244bb1a85191e63c32b9ba4ea91d5a065
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
index 5e02d8e..999c318 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
@@ -1 +1 @@
-9e7e068fe412696696c601d610dd15ca31d23d213223595b87951ee566479416
\ No newline at end of file
+82b026182d09d325c15160185b636917a54ab2431f0f7cb493cd5341bd7798c5
\ No newline at end of file
diff --git a/ui/src/common/addEphemeralTab.ts b/ui/src/common/addEphemeralTab.ts
index d04148c..066b5e7 100644
--- a/ui/src/common/addEphemeralTab.ts
+++ b/ui/src/common/addEphemeralTab.ts
@@ -14,14 +14,28 @@
import {uuidv4} from '../base/uuid';
import {BottomTab} from '../frontend/bottom_tab';
import {globals} from '../frontend/globals';
+import {Tab} from '../public';
import {BottomTabToTabAdapter} from '../public/utils';
+
import {Actions} from './actions';
-export function addEphemeralTab(tab: BottomTab, uriPrefix: string): void {
+export function addEphemeralTab(uriPrefix: string, tab: Tab): void {
const uri = `${uriPrefix}#${uuidv4()}`;
globals.tabManager.registerTab({
uri,
+ content: tab,
+ isEphemeral: true,
+ });
+
+ globals.dispatch(Actions.showTab({uri}));
+}
+
+export function addBottomTab(tab: BottomTab, uriPrefix: string): void {
+ const uri = `${uriPrefix}#${tab.uuid}`;
+
+ globals.tabManager.registerTab({
+ uri,
content: new BottomTabToTabAdapter(tab),
isEphemeral: true,
});
diff --git a/ui/src/frontend/charts/histogram/tab.ts b/ui/src/frontend/charts/histogram/tab.ts
index 78a58b5..e5618eb 100644
--- a/ui/src/frontend/charts/histogram/tab.ts
+++ b/ui/src/frontend/charts/histogram/tab.ts
@@ -14,16 +14,17 @@
import m from 'mithril';
-import {DetailsShell} from '../../../widgets/details_shell';
-import {uuidv4} from '../../../base/uuid';
-import {BottomTab, NewBottomTabArgs} from '../../bottom_tab';
-import {VegaView} from '../../../widgets/vega_view';
-import {addEphemeralTab} from '../../../common/addEphemeralTab';
-import {HistogramState} from './state';
import {stringifyJsonWithBigints} from '../../../base/json_utils';
+import {uuidv4} from '../../../base/uuid';
+import {addBottomTab} from '../../../common/addEphemeralTab';
import {Engine} from '../../../public';
+import {DetailsShell} from '../../../widgets/details_shell';
+import {VegaView} from '../../../widgets/vega_view';
+import {BottomTab, NewBottomTabArgs} from '../../bottom_tab';
import {Filter, filterTitle} from '../../widgets/sql/table2/column';
+import {HistogramState} from './state';
+
interface HistogramTabConfig {
columnTitle: string; // Human readable column name (ex: Duration)
sqlColumn: string; // SQL column name (ex: dur)
@@ -42,7 +43,7 @@
uuid: uuidv4(),
});
- addEphemeralTab(histogramTab, 'histogramTab');
+ addBottomTab(histogramTab, 'histogramTab');
}
export class HistogramTab extends BottomTab<HistogramTabConfig> {
diff --git a/ui/src/frontend/get_engine.ts b/ui/src/frontend/get_engine.ts
new file mode 100644
index 0000000..4ea287c
--- /dev/null
+++ b/ui/src/frontend/get_engine.ts
@@ -0,0 +1,25 @@
+// Copyright (C) 2024 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 {assertExists} from '../base/logging';
+import {Engine} from '../public';
+
+import {globals} from './globals';
+
+// TODO(stevegolton): Find a way to make this more elegant.
+export function getEngine(tag: string): Engine {
+ const engConfig = globals.getCurrentEngine();
+ const engineId = assertExists(engConfig).id;
+ return assertExists(globals.engines.get(engineId)).getProxy(tag);
+}
diff --git a/ui/src/frontend/process_details_tab.ts b/ui/src/frontend/process_details_tab.ts
new file mode 100644
index 0000000..3a1274c
--- /dev/null
+++ b/ui/src/frontend/process_details_tab.ts
@@ -0,0 +1,79 @@
+// Copyright (C) 2024 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 m from 'mithril';
+
+import {Engine, Tab} from '../public';
+import {Upid} from '../trace_processor/sql_utils/core_types';
+import {DetailsShell} from '../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../widgets/grid_layout';
+import {Section} from '../widgets/section';
+
+import {Details, DetailsSchema} from './widgets/sql/details/details';
+import {wellKnownTypes} from './widgets/sql/details/well_known_types';
+
+import d = DetailsSchema;
+
+export class ProcessDetailsTab implements Tab {
+ private data: Details;
+
+ // TODO(altimin): Ideally, we would not require the pid to be passed in, but
+ // fetch it from the underlying data instead.
+ //
+ // However, the only place which creates `ProcessDetailsTab` currently is `renderProcessRef`,
+ // which already has `pid` available (note that Details is already fetching the data, including
+ // the `pid` from the trace processor, but it doesn't expose it for now).
+ constructor(private args: {engine: Engine; upid: Upid; pid?: number}) {
+ this.data = new Details(
+ args.engine,
+ 'process',
+ args.upid,
+ {
+ 'pid': d.Value('pid'),
+ 'Name': d.Value('name'),
+ 'Start time': d.Timestamp('start_ts', {skipIfNull: true}),
+ 'End time': d.Timestamp('end_ts', {skipIfNull: true}),
+ 'Parent process': d.SqlIdRef('process', 'parent_upid', {
+ skipIfNull: true,
+ }),
+ 'User ID': d.Value('uid', {skipIfNull: true}),
+ 'Android app ID': d.Value('android_appid', {skipIfNull: true}),
+ 'Command line': d.Value('cmdline', {skipIfNull: true}),
+ 'Machine id': d.Value('machine_id', {skipIfNull: true}),
+ 'Args': d.ArgSetId('arg_set_id'),
+ },
+ wellKnownTypes,
+ );
+ }
+
+ render() {
+ return m(
+ DetailsShell,
+ {
+ title: this.getTitle(),
+ },
+ m(
+ GridLayout,
+ m(GridLayoutColumn, m(Section, {title: 'Details'}, this.data.render())),
+ ),
+ );
+ }
+
+ getTitle(): string {
+ if (this.args.pid !== undefined) {
+ return `Process ${this.args.pid}`;
+ }
+ return `Process upid:${this.args.upid}`;
+ }
+}
diff --git a/ui/src/frontend/slice_details.ts b/ui/src/frontend/slice_details.ts
index c284bfb..37e770f 100644
--- a/ui/src/frontend/slice_details.ts
+++ b/ui/src/frontend/slice_details.ts
@@ -17,23 +17,23 @@
import {BigintMath} from '../base/bigint_math';
import {sqliteString} from '../base/string_utils';
import {exists} from '../base/utils';
+import {SliceDetails} from '../trace_processor/sql_utils/slice';
import {Anchor} from '../widgets/anchor';
import {MenuItem, PopupMenu2} from '../widgets/menu';
import {Section} from '../widgets/section';
import {SqlRef} from '../widgets/sql_ref';
import {Tree, TreeNode} from '../widgets/tree';
-import {SliceDetails} from '../trace_processor/sql_utils/slice';
import {
BreakdownByThreadState,
BreakdownByThreadStateTreeNode,
} from './sql/thread_state';
-import {DurationWidget} from './widgets/duration';
-import {Timestamp} from './widgets/timestamp';
import {addSqlTableTab} from './sql_table_tab';
-import {getThreadName} from '../trace_processor/sql_utils/thread';
-import {getProcessName} from '../trace_processor/sql_utils/process';
+import {DurationWidget} from './widgets/duration';
+import {renderProcessRef} from './widgets/process';
import {SqlTables} from './widgets/sql/table2/well_known_sql_tables';
+import {renderThreadRef} from './widgets/thread';
+import {Timestamp} from './widgets/timestamp';
// Renders a widget storing all of the generic details for a slice from the
// slice table.
@@ -99,12 +99,12 @@
slice.thread &&
m(TreeNode, {
left: 'Thread',
- right: getThreadName(slice.thread),
+ right: renderThreadRef(slice.thread),
}),
slice.process &&
m(TreeNode, {
left: 'Process',
- right: getProcessName(slice.process),
+ right: renderProcessRef(slice.process),
}),
slice.process &&
exists(slice.process.uid) &&
diff --git a/ui/src/frontend/sql_table_tab.ts b/ui/src/frontend/sql_table_tab.ts
index 95c6b1e..7df282d 100644
--- a/ui/src/frontend/sql_table_tab.ts
+++ b/ui/src/frontend/sql_table_tab.ts
@@ -17,18 +17,17 @@
import {copyToClipboard} from '../base/clipboard';
import {Icons} from '../base/semantic_icons';
import {exists} from '../base/utils';
+import {uuidv4} from '../base/uuid';
+import {addBottomTab} from '../common/addEphemeralTab';
import {Button} from '../widgets/button';
import {DetailsShell} from '../widgets/details_shell';
import {Popup, PopupPosition} from '../widgets/popup';
-import {Engine} from '../public';
-import {assertExists} from '../base/logging';
-import {uuidv4} from '../base/uuid';
-import {addEphemeralTab} from '../common/addEphemeralTab';
+
import {BottomTab, NewBottomTabArgs} from './bottom_tab';
-import {globals} from './globals';
import {AddDebugTrackMenu} from './debug_tracks/add_debug_track_menu';
-import {SqlTableDescription, SqlTableState} from './widgets/sql/table2/state';
+import {getEngine} from './get_engine';
import {Filter} from './widgets/sql/table2/column';
+import {SqlTableDescription, SqlTableState} from './widgets/sql/table2/state';
import {SqlTable} from './widgets/sql/table2/table';
interface SqlTableTabConfig {
@@ -40,18 +39,11 @@
export function addSqlTableTab(config: SqlTableTabConfig): void {
const queryResultsTab = new SqlTableTab({
config,
- engine: getEngine(),
+ engine: getEngine('QueryResult'),
uuid: uuidv4(),
});
- addEphemeralTab(queryResultsTab, 'sqlTable');
-}
-
-// TODO(stevegolton): Find a way to make this more elegant.
-function getEngine(): Engine {
- const engConfig = globals.getCurrentEngine();
- const engineId = assertExists(engConfig).id;
- return assertExists(globals.engines.get(engineId)).getProxy('QueryResult');
+ addBottomTab(queryResultsTab, 'sqlTable');
}
export class SqlTableTab extends BottomTab<SqlTableTabConfig> {
diff --git a/ui/src/frontend/thread_details_tab.ts b/ui/src/frontend/thread_details_tab.ts
new file mode 100644
index 0000000..be638a8
--- /dev/null
+++ b/ui/src/frontend/thread_details_tab.ts
@@ -0,0 +1,71 @@
+// Copyright (C) 2024 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 m from 'mithril';
+
+import {Engine, Tab} from '../public';
+import {Utid} from '../trace_processor/sql_utils/core_types';
+import {DetailsShell} from '../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../widgets/grid_layout';
+import {Section} from '../widgets/section';
+
+import {Details, DetailsSchema} from './widgets/sql/details/details';
+import {wellKnownTypes} from './widgets/sql/details/well_known_types';
+
+import d = DetailsSchema;
+
+export class ThreadDetailsTab implements Tab {
+ private data: Details;
+
+ // TODO(altimin): Ideally, we would not require the tid to be passed in, but
+ // fetch it from the underlying data instead. See comment in ProcessDetailsTab
+ // for more details.
+ constructor(private args: {engine: Engine; utid: Utid; tid?: number}) {
+ this.data = new Details(
+ args.engine,
+ 'thread',
+ args.utid,
+ {
+ 'tid': d.Value('tid'),
+ 'Name': d.Value('name'),
+ 'Process': d.SqlIdRef('process', 'upid'),
+ 'Is main thread': d.Boolean('is_main_thread'),
+ 'Start time': d.Timestamp('start_ts', {skipIfNull: true}),
+ 'End time': d.Timestamp('end_ts', {skipIfNull: true}),
+ 'Machine id': d.Value('machine_id', {skipIfNull: true}),
+ },
+ wellKnownTypes,
+ );
+ }
+
+ render() {
+ return m(
+ DetailsShell,
+ {
+ title: this.getTitle(),
+ },
+ m(
+ GridLayout,
+ m(GridLayoutColumn, m(Section, {title: 'Details'}, this.data.render())),
+ ),
+ );
+ }
+
+ getTitle(): string {
+ if (this.args.tid !== undefined) {
+ return `Thread ${this.args.tid}`;
+ }
+ return `Thread utid:${this.args.utid}`;
+ }
+}
diff --git a/ui/src/frontend/widgets/process.ts b/ui/src/frontend/widgets/process.ts
index f7bcb93..defc1f1 100644
--- a/ui/src/frontend/widgets/process.ts
+++ b/ui/src/frontend/widgets/process.ts
@@ -13,15 +13,19 @@
// limitations under the License.
import m from 'mithril';
-import {
- ProcessInfo,
- getProcessName,
-} from '../../trace_processor/sql_utils/process';
-import {MenuItem, PopupMenu2} from '../../widgets/menu';
-import {Anchor} from '../../widgets/anchor';
-import {exists} from '../../base/utils';
-import {Icons} from '../../base/semantic_icons';
+
import {copyToClipboard} from '../../base/clipboard';
+import {Icons} from '../../base/semantic_icons';
+import {exists} from '../../base/utils';
+import {addEphemeralTab} from '../../common/addEphemeralTab';
+import {
+ getProcessName,
+ ProcessInfo,
+} from '../../trace_processor/sql_utils/process';
+import {Anchor} from '../../widgets/anchor';
+import {MenuItem, PopupMenu2} from '../../widgets/menu';
+import {getEngine} from '../get_engine';
+import {ProcessDetailsTab} from '../process_details_tab';
export function renderProcessRef(info: ProcessInfo): m.Children {
const name = info.name;
@@ -47,5 +51,18 @@
label: 'Copy upid',
onclick: () => copyToClipboard(`${info.upid}`),
}),
+ m(MenuItem, {
+ icon: Icons.ExternalLink,
+ label: 'Show process details',
+ onclick: () =>
+ addEphemeralTab(
+ 'processDetails',
+ new ProcessDetailsTab({
+ engine: getEngine('processDetails'),
+ upid: info.upid,
+ pid: info.pid,
+ }),
+ ),
+ }),
);
}
diff --git a/ui/src/frontend/widgets/sql/details/details.ts b/ui/src/frontend/widgets/sql/details/details.ts
index 09b7f4c..31e4b79 100644
--- a/ui/src/frontend/widgets/sql/details/details.ts
+++ b/ui/src/frontend/widgets/sql/details/details.ts
@@ -168,6 +168,13 @@
return new ScalarValueSchema('url', value, args);
}
+ export function Boolean(
+ value: string,
+ args?: ScalarValueParams,
+ ): ScalarValueSchema {
+ return new ScalarValueSchema('boolean', value, args);
+ }
+
// Create an object representing a reference to a SQL table row in the schema.
// |table| - name of the table.
// |id| - SQL expression (e.g. column name) for the id.
@@ -339,7 +346,13 @@
// from SQL).
class ScalarValueSchema {
constructor(
- public kind: 'timestamp' | 'duration' | 'arg_set_id' | 'value' | 'url',
+ public kind:
+ | 'timestamp'
+ | 'duration'
+ | 'arg_set_id'
+ | 'value'
+ | 'url'
+ | 'boolean',
public sourceExpression: string,
public params?: ScalarValueParams,
) {}
@@ -347,7 +360,7 @@
// Resolved version of simple scalar values.
type ResolvedScalarValue = {
- kind: 'timestamp' | 'duration' | 'value' | 'url';
+ kind: 'timestamp' | 'duration' | 'value' | 'url' | 'boolean';
source: ExpressionIndex;
} & ScalarValueParams;
@@ -435,7 +448,13 @@
// Source statements for the SQL references.
sqlIdRefs: {tableName: string; idExpression: string}[];
// Fetched data for the SQL references.
- sqlIdRefData: ({data: {}; id: bigint} | Err)[];
+ sqlIdRefData: (
+ | {
+ data: {};
+ id: bigint | null;
+ }
+ | Err
+ )[];
}
// Class responsible for collecting the description of the data to fetch and
@@ -501,7 +520,7 @@
const argSetId = data.values[argSetIndex];
if (argSetId === null) {
data.argSets.push([]);
- } else if (typeof argSetId !== 'number') {
+ } else if (typeof argSetId !== 'number' && typeof argSetId !== 'bigint') {
data.argSets.push(
new Err(
`Incorrect type for arg set ${
@@ -510,7 +529,9 @@
),
);
} else {
- data.argSets.push(await getArgs(this.engine, asArgSetId(argSetId)));
+ data.argSets.push(
+ await getArgs(this.engine, asArgSetId(Number(argSetId))),
+ );
}
}
@@ -522,7 +543,10 @@
continue;
}
const id = data.values[ref.id];
- if (typeof id !== 'bigint') {
+ if (id === null) {
+ data.sqlIdRefData.push({data: {}, id});
+ continue;
+ } else if (typeof id !== 'bigint') {
data.sqlIdRefData.push(
new Err(
`Incorrect type for SQL reference ${
@@ -673,10 +697,10 @@
});
case 'url': {
const url = data.values[value.source];
- let rhs: m.Child;
+ let rhs: m.Children;
if (url === null) {
if (value.skipIfNull) return null;
- rhs = m('i', 'NULL');
+ rhs = renderNull();
} else if (typeof url !== 'string') {
rhs = renderError(
`Incorrect type for URL ${
@@ -695,6 +719,21 @@
right: rhs,
});
}
+ case 'boolean': {
+ const bool = data.values[value.source];
+ if (bool === null && value.skipIfNull) return null;
+ let rhs: m.Child;
+ if (typeof bool !== 'bigint' && typeof bool !== 'number') {
+ rhs = renderError(
+ `Incorrect type for boolean ${
+ data.valueExpressions[value.source]
+ }: expected bigint or number, got ${typeof bool}`,
+ );
+ } else {
+ rhs = bool ? 'true' : 'false';
+ }
+ return m(TreeNode, {left: key, right: rhs});
+ }
case 'timestamp': {
const ts = data.values[value.source];
let rhs: m.Child;
@@ -747,6 +786,8 @@
let children: m.Children;
if (refData instanceof Err) {
rhs = renderError(refData.message);
+ } else if (refData.id === null && value.skipIfNull === true) {
+ rhs = renderNull();
} else {
const renderer = sqlIdRefRenderers[ref.tableName];
if (renderer === undefined) {
@@ -828,3 +869,7 @@
}
}
}
+
+function renderNull(): m.Children {
+ return m('i', 'NULL');
+}
diff --git a/ui/src/frontend/widgets/thread.ts b/ui/src/frontend/widgets/thread.ts
index b668d1c..1254a0b 100644
--- a/ui/src/frontend/widgets/thread.ts
+++ b/ui/src/frontend/widgets/thread.ts
@@ -14,15 +14,18 @@
import m from 'mithril';
-import {
- ThreadInfo,
- getThreadName,
-} from '../../trace_processor/sql_utils/thread';
-import {MenuItem, PopupMenu2} from '../../widgets/menu';
-import {Anchor} from '../../widgets/anchor';
-import {exists} from '../../base/utils';
-import {Icons} from '../../base/semantic_icons';
import {copyToClipboard} from '../../base/clipboard';
+import {Icons} from '../../base/semantic_icons';
+import {exists} from '../../base/utils';
+import {addEphemeralTab} from '../../common/addEphemeralTab';
+import {
+ getThreadName,
+ ThreadInfo,
+} from '../../trace_processor/sql_utils/thread';
+import {Anchor} from '../../widgets/anchor';
+import {MenuItem, PopupMenu2} from '../../widgets/menu';
+import {getEngine} from '../get_engine';
+import {ThreadDetailsTab} from '../thread_details_tab';
export function renderThreadRef(info: ThreadInfo): m.Children {
const name = info.name;
@@ -48,5 +51,18 @@
label: 'Copy utid',
onclick: () => copyToClipboard(`${info.utid}`),
}),
+ m(MenuItem, {
+ icon: Icons.ExternalLink,
+ label: 'Show thread details',
+ onclick: () =>
+ addEphemeralTab(
+ 'threadDetails',
+ new ThreadDetailsTab({
+ engine: getEngine('ThreadDetails'),
+ utid: info.utid,
+ tid: info.tid,
+ }),
+ ),
+ }),
);
}