| // 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); |
| } |