xref: /aosp_15_r20/external/perfetto/ui/src/components/details/slice_args.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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