1// Copyright (C) 2023 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'; 16 17import {isString} from '../base/object_utils'; 18import {Icons} from '../base/semantic_icons'; 19import {sqliteString} from '../base/string_utils'; 20import {exists} from '../base/utils'; 21import {ArgNode, convertArgsToTree, Key} from '../controller/args_parser'; 22import {Engine} from '../trace_processor/engine'; 23import {addVisualisedArgTracks} from './visualized_args_tracks'; 24import {Anchor} from '../widgets/anchor'; 25import {MenuItem, PopupMenu2} from '../widgets/menu'; 26import {TreeNode} from '../widgets/tree'; 27 28import {Arg} from './sql/args'; 29import {addSqlTableTab} from './sql_table/tab'; 30import {SqlTables} from './sql_table/well_known_tables'; 31import {globals} from './globals'; 32 33// Renders slice arguments (key/value pairs) as a subtree. 34export function renderArguments(engine: Engine, args: Arg[]): m.Children { 35 if (args.length > 0) { 36 const tree = convertArgsToTree(args); 37 return renderArgTreeNodes(engine, tree); 38 } else { 39 return undefined; 40 } 41} 42 43export function hasArgs(args?: Arg[]): args is Arg[] { 44 return exists(args) && args.length > 0; 45} 46 47function renderArgTreeNodes(engine: Engine, args: ArgNode<Arg>[]): m.Children { 48 return args.map((arg) => { 49 const {key, value, children} = arg; 50 if (children && children.length === 1) { 51 // If we only have one child, collapse into self and combine keys 52 const child = children[0]; 53 const compositeArg = { 54 ...child, 55 key: stringifyKey(key, child.key), 56 }; 57 return renderArgTreeNodes(engine, [compositeArg]); 58 } else { 59 return m( 60 TreeNode, 61 { 62 left: renderArgKey(engine, stringifyKey(key), value), 63 right: exists(value) && renderArgValue(value), 64 summary: children && renderSummary(children), 65 }, 66 children && renderArgTreeNodes(engine, children), 67 ); 68 } 69 }); 70} 71 72function renderArgKey(engine: Engine, key: string, value?: Arg): m.Children { 73 if (value === undefined) { 74 return key; 75 } else { 76 const {key: fullKey, displayValue} = value; 77 return m( 78 PopupMenu2, 79 {trigger: m(Anchor, {icon: Icons.ContextMenu}, key)}, 80 m(MenuItem, { 81 label: 'Copy full key', 82 icon: 'content_copy', 83 onclick: () => navigator.clipboard.writeText(fullKey), 84 }), 85 m(MenuItem, { 86 label: 'Find slices with same arg value', 87 icon: 'search', 88 onclick: () => { 89 addSqlTableTab({ 90 table: SqlTables.slice, 91 filters: [ 92 { 93 type: 'arg_filter', 94 argSetIdColumn: 'arg_set_id', 95 argName: fullKey, 96 op: `= ${sqliteString(displayValue)}`, 97 }, 98 ], 99 }); 100 }, 101 }), 102 m(MenuItem, { 103 label: 'Visualise argument values', 104 icon: 'query_stats', 105 onclick: () => { 106 addVisualisedArgTracks( 107 { 108 engine, 109 registerTrack: (t) => globals.trackManager.registerTrack(t), 110 }, 111 fullKey, 112 ); 113 }, 114 }), 115 ); 116 } 117} 118 119function renderArgValue({value}: Arg): m.Children { 120 if (isWebLink(value)) { 121 return renderWebLink(value); 122 } else { 123 return `${value}`; 124 } 125} 126 127function renderSummary(children: ArgNode<Arg>[]): m.Children { 128 const summary = children 129 .slice(0, 2) 130 .map(({key}) => key) 131 .join(', '); 132 const remaining = children.length - 2; 133 if (remaining > 0) { 134 return `{${summary}, ... (${remaining} more items)}`; 135 } else { 136 return `{${summary}}`; 137 } 138} 139 140function stringifyKey(...key: Key[]): string { 141 return key 142 .map((element, index) => { 143 if (typeof element === 'number') { 144 return `[${element}]`; 145 } else { 146 return (index === 0 ? '' : '.') + element; 147 } 148 }) 149 .join(''); 150} 151 152function isWebLink(value: unknown): value is string { 153 return ( 154 isString(value) && 155 (value.startsWith('http://') || value.startsWith('https://')) 156 ); 157} 158 159function renderWebLink(url: string): m.Children { 160 return m(Anchor, {href: url, target: '_blank', icon: 'open_in_new'}, url); 161} 162