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'; 16import {isString} from '../../base/object_utils'; 17import {Icons} from '../../base/semantic_icons'; 18import {sqliteString} from '../../base/string_utils'; 19import {exists} from '../../base/utils'; 20import {ArgNode, convertArgsToTree, Key} from './slice_args_parser'; 21import {Anchor} from '../../widgets/anchor'; 22import {MenuItem, PopupMenu2} from '../../widgets/menu'; 23import {TreeNode} from '../../widgets/tree'; 24import {Arg} from '../sql_utils/args'; 25import {assertExists} from '../../base/logging'; 26import {getSqlTableDescription} from '../widgets/sql/table/sql_table_registry'; 27import {Trace} from '../../public/trace'; 28import {extensions} from '../extensions'; 29 30// Renders slice arguments (key/value pairs) as a subtree. 31export function renderArguments(trace: Trace, args: Arg[]): m.Children { 32 if (args.length > 0) { 33 const tree = convertArgsToTree(args); 34 return renderArgTreeNodes(trace, tree); 35 } else { 36 return undefined; 37 } 38} 39 40export function hasArgs(args?: Arg[]): args is Arg[] { 41 return exists(args) && args.length > 0; 42} 43 44function renderArgTreeNodes(trace: Trace, args: ArgNode<Arg>[]): m.Children { 45 return args.map((arg) => { 46 const {key, value, children} = arg; 47 if (children && children.length === 1) { 48 // If we only have one child, collapse into self and combine keys 49 const child = children[0]; 50 const compositeArg = { 51 ...child, 52 key: stringifyKey(key, child.key), 53 }; 54 return renderArgTreeNodes(trace, [compositeArg]); 55 } else { 56 return m( 57 TreeNode, 58 { 59 left: renderArgKey(trace, stringifyKey(key), value), 60 right: exists(value) && renderArgValue(value), 61 summary: children && renderSummary(children), 62 }, 63 children && renderArgTreeNodes(trace, children), 64 ); 65 } 66 }); 67} 68 69function renderArgKey(trace: Trace, key: string, value?: Arg): m.Children { 70 if (value === undefined) { 71 return key; 72 } else { 73 const {key: fullKey, displayValue} = value; 74 return m( 75 PopupMenu2, 76 {trigger: m(Anchor, {icon: Icons.ContextMenu}, key)}, 77 m(MenuItem, { 78 label: 'Copy full key', 79 icon: 'content_copy', 80 onclick: () => navigator.clipboard.writeText(fullKey), 81 }), 82 m(MenuItem, { 83 label: 'Find slices with same arg value', 84 icon: 'search', 85 onclick: () => { 86 extensions.addSqlTableTab(trace, { 87 table: assertExists(getSqlTableDescription('slice')), 88 filters: [ 89 { 90 op: (cols) => `${cols[0]} = ${sqliteString(displayValue)}`, 91 columns: [ 92 { 93 column: 'display_value', 94 source: { 95 table: 'args', 96 joinOn: { 97 arg_set_id: 'arg_set_id', 98 key: sqliteString(fullKey), 99 }, 100 }, 101 }, 102 ], 103 }, 104 ], 105 }); 106 }, 107 }), 108 m(MenuItem, { 109 label: 'Visualize argument values', 110 icon: 'query_stats', 111 onclick: () => { 112 extensions.addVisualizedArgTracks(trace, fullKey); 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