blob: 9d4c122075c0240e276c559669749034f1fa8bba [file] [log] [blame]
// Copyright (C) 2023 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 {v4 as uuidv4} from 'uuid';
import {isString} from '../base/object_utils';
import {Icons} from '../base/semantic_icons';
import {sqliteString} from '../base/string_utils';
import {exists} from '../base/utils';
import {Actions, AddTrackArgs} from '../common/actions';
import {InThreadTrackSortKey} from '../common/state';
import {ArgNode, convertArgsToTree, Key} from '../controller/args_parser';
import {EngineProxy} from '../trace_processor/engine';
import {NUM} from '../trace_processor/query_result';
import {
VISUALISED_ARGS_SLICE_TRACK_URI,
VisualisedArgsState,
} from './visualized_args_tracks';
import {Anchor} from '../widgets/anchor';
import {MenuItem, PopupMenu2} from '../widgets/menu';
import {TreeNode} from '../widgets/tree';
import {addTab} from './bottom_tab';
import {globals} from './globals';
import {Arg} from './sql/args';
import {SqlTableTab} from './sql_table/tab';
import {SqlTables} from './sql_table/well_known_tables';
// Renders slice arguments (key/value pairs) as a subtree.
export function renderArguments(engine: EngineProxy, args: Arg[]): m.Children {
if (args.length > 0) {
const tree = convertArgsToTree(args);
return renderArgTreeNodes(engine, tree);
} else {
return undefined;
}
}
export function hasArgs(args?: Arg[]): args is Arg[] {
return exists(args) && args.length > 0;
}
function renderArgTreeNodes(
engine: EngineProxy, args: ArgNode<Arg>[]): m.Children {
return args.map((arg) => {
const {key, value, children} = arg;
if (children && children.length === 1) {
// If we only have one child, collapse into self and combine keys
const child = children[0];
const compositeArg = {
...child,
key: stringifyKey(key, child.key),
};
return renderArgTreeNodes(engine, [compositeArg]);
} else {
return m(
TreeNode,
{
left: renderArgKey(engine, stringifyKey(key), value),
right: exists(value) && renderArgValue(value),
summary: children && renderSummary(children),
},
children && renderArgTreeNodes(engine, children),
);
}
});
}
function renderArgKey(
engine: EngineProxy, key: string, value?: Arg): m.Children {
if (value === undefined) {
return key;
} else {
const {key: fullKey, displayValue} = value;
return m(
PopupMenu2,
{trigger: m(Anchor, {icon: Icons.ContextMenu}, key)},
m(MenuItem, {
label: 'Copy full key',
icon: 'content_copy',
onclick: () => navigator.clipboard.writeText(fullKey),
}),
m(MenuItem, {
label: 'Find slices with same arg value',
icon: 'search',
onclick: () => {
addTab({
kind: SqlTableTab.kind,
config: {
table: SqlTables.slice,
filters: [{
type: 'arg_filter',
argSetIdColumn: 'arg_set_id',
argName: fullKey,
op: `= ${sqliteString(displayValue)}`,
}],
},
});
},
}),
m(MenuItem, {
label: 'Visualise argument values',
icon: 'query_stats',
onclick: () => {
addVisualisedArg(engine, fullKey);
},
}),
);
}
}
async function addVisualisedArg(engine: EngineProxy, argName: string) {
const escapedArgName = argName.replace(/[^a-zA-Z]/g, '_');
const tableName = `__arg_visualisation_helper_${escapedArgName}_slice`;
const result = await engine.query(`
drop table if exists ${tableName};
create table ${tableName} as
with slice_with_arg as (
select
slice.id,
slice.track_id,
slice.ts,
slice.dur,
slice.thread_dur,
NULL as cat,
args.display_value as name
from slice
join args using (arg_set_id)
where args.key='${argName}'
)
select
*,
(select count()
from ancestor_slice(s1.id) s2
join slice_with_arg s3 on s2.id=s3.id
) as depth
from slice_with_arg s1
order by id;
select
track_id as trackId,
max(depth) as maxDepth
from ${tableName}
group by track_id;
`);
const tracksToAdd: AddTrackArgs[] = [];
const it = result.iter({'trackId': NUM, 'maxDepth': NUM});
const addedTrackKeys: string[] = [];
for (; it.valid(); it.next()) {
const track =
globals.state.tracks[globals.state.trackKeyByTrackId[it.trackId]];
const utid = (track.trackSortKey as {utid?: number}).utid;
const key = uuidv4();
addedTrackKeys.push(key);
const params: VisualisedArgsState = {
maxDepth: it.maxDepth,
trackId: it.trackId,
argName: argName,
};
tracksToAdd.push({
key,
trackGroup: track.trackGroup,
name: argName,
trackSortKey: utid === undefined ?
track.trackSortKey :
{utid, priority: InThreadTrackSortKey.VISUALISED_ARGS_TRACK},
params,
uri: VISUALISED_ARGS_SLICE_TRACK_URI,
});
}
globals.dispatchMultiple([
Actions.addTracks({tracks: tracksToAdd}),
Actions.sortThreadTracks({}),
]);
}
function renderArgValue({value}: Arg): m.Children {
if (isWebLink(value)) {
return renderWebLink(value);
} else {
return `${value}`;
}
}
function renderSummary(children: ArgNode<Arg>[]): m.Children {
const summary = children.slice(0, 2).map(({key}) => key).join(', ');
const remaining = children.length - 2;
if (remaining > 0) {
return `{${summary}, ... (${remaining} more items)}`;
} else {
return `{${summary}}`;
}
}
function stringifyKey(...key: Key[]): string {
return key
.map((element, index) => {
if (typeof element === 'number') {
return `[${element}]`;
} else {
return (index === 0 ? '' : '.') + element;
}
})
.join('');
}
function isWebLink(value: unknown): value is string {
return isString(value) &&
(value.startsWith('http://') || value.startsWith('https://'));
}
function renderWebLink(url: string): m.Children {
return m(Anchor, {href: url, target: '_blank', icon: 'open_in_new'}, url);
}