1// Copyright (C) 2019 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use size 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 * as m from 'mithril'; 16 17import {Actions} from '../common/actions'; 18import {Arg, ArgsTree, isArgTreeArray, isArgTreeMap} from '../common/arg_types'; 19import {timeToCode, toNs} from '../common/time'; 20 21import {globals, SliceDetails} from './globals'; 22import {Panel, PanelSize} from './panel'; 23import {verticalScrollToTrack} from './scroll_helper'; 24 25// Table row contents is one of two things: 26// 1. Key-value pair 27interface KVPair { 28 kind: 'KVPair'; 29 key: string; 30 value: Arg; 31} 32 33// 2. Common prefix for values in an array 34interface TableHeader { 35 kind: 'TableHeader'; 36 header: string; 37} 38 39type RowContents = KVPair|TableHeader; 40 41function isTableHeader(contents: RowContents): contents is TableHeader { 42 return contents.kind === 'TableHeader'; 43} 44 45interface Row { 46 // How many columns (empty or with an index) precede a key 47 indentLevel: number; 48 // Index if the current row is an element of array 49 index: number; 50 contents: RowContents; 51} 52 53class TableBuilder { 54 // Stack contains indices inside repeated fields, or -1 if the appropriate 55 // index is already displayed. 56 stack: number[] = []; 57 58 // Row data generated by builder 59 rows: Row[] = []; 60 61 // Maximum indent level of a key, used to determine total number of columns 62 maxIndent = 0; 63 64 // Add a key-value pair into the table 65 add(key: string, value: Arg) { 66 this.rows.push( 67 {indentLevel: 0, index: -1, contents: {kind: 'KVPair', key, value}}); 68 } 69 70 // Add arguments tree into the table 71 addTree(tree: ArgsTree) { 72 this.addTreeInternal(tree, ''); 73 } 74 75 // Return indent level and index for a fresh row 76 private prepareRow(): [number, number] { 77 const level = this.stack.length; 78 let index = -1; 79 if (level > 0) { 80 index = this.stack[level - 1]; 81 if (index !== -1) { 82 this.stack[level - 1] = -1; 83 } 84 } 85 this.maxIndent = Math.max(this.maxIndent, level); 86 return [level, index]; 87 } 88 89 private addTreeInternal(record: ArgsTree, prefix: string) { 90 if (isArgTreeArray(record)) { 91 // Add the current prefix as a separate row 92 const row = this.prepareRow(); 93 this.rows.push({ 94 indentLevel: row[0], 95 index: row[1], 96 contents: {kind: 'TableHeader', header: prefix} 97 }); 98 99 for (let i = 0; i < record.length; i++) { 100 // Push the current array index to the stack. 101 this.stack.push(i); 102 // Prefix is empty for array elements because we don't want to repeat 103 // the common prefix 104 this.addTreeInternal(record[i], ''); 105 this.stack.pop(); 106 } 107 } else if (isArgTreeMap(record)) { 108 for (const [key, value] of Object.entries(record)) { 109 // If the prefix was non-empty, we have to add dot at the end as well. 110 const newPrefix = (prefix === '') ? key : prefix + '.' + key; 111 this.addTreeInternal(value, newPrefix); 112 } 113 } else { 114 // Leaf value in the tree: add to the table 115 const row = this.prepareRow(); 116 this.rows.push({ 117 indentLevel: row[0], 118 index: row[1], 119 contents: {kind: 'KVPair', key: prefix, value: record} 120 }); 121 } 122 } 123} 124 125export class ChromeSliceDetailsPanel extends Panel { 126 view() { 127 const sliceInfo = globals.sliceDetails; 128 if (sliceInfo.ts !== undefined && sliceInfo.dur !== undefined && 129 sliceInfo.name !== undefined) { 130 const builder = new TableBuilder(); 131 builder.add('Name', sliceInfo.name); 132 builder.add( 133 'Category', 134 !sliceInfo.category || sliceInfo.category === '[NULL]' ? 135 'N/A' : 136 sliceInfo.category); 137 builder.add('Start time', timeToCode(sliceInfo.ts)); 138 builder.add( 139 'Duration', 140 toNs(sliceInfo.dur) === -1 ? '-1 (Did not end)' : 141 timeToCode(sliceInfo.dur)); 142 if (sliceInfo.description) { 143 this.fillDescription(sliceInfo.description, builder); 144 } 145 this.fillArgs(sliceInfo, builder); 146 return m( 147 '.details-panel', 148 m('.details-panel-heading', m('h2', `Slice Details`)), 149 m('.details-table', this.renderTable(builder))); 150 } else { 151 return m( 152 '.details-panel', 153 m('.details-panel-heading', 154 m( 155 'h2', 156 `Slice Details`, 157 ))); 158 } 159 } 160 161 renderCanvas(_ctx: CanvasRenderingContext2D, _size: PanelSize) {} 162 163 fillArgs(slice: SliceDetails, builder: TableBuilder) { 164 if (slice.argsTree && slice.args) { 165 // Parsed arguments are available, need only to iterate over them to get 166 // slice references 167 for (const [key, value] of slice.args) { 168 if (typeof value !== 'string') { 169 builder.add(key, value); 170 } 171 } 172 builder.addTree(slice.argsTree); 173 } else if (slice.args) { 174 // Parsing has failed, but arguments are available: display them in a flat 175 // 2-column table 176 for (const [key, value] of slice.args) { 177 builder.add(key, value); 178 } 179 } 180 } 181 182 renderTable(builder: TableBuilder): m.Vnode { 183 const rows: m.Vnode[] = []; 184 const keyColumnCount = builder.maxIndent + 1; 185 for (const row of builder.rows) { 186 const renderedRow: m.Vnode[] = []; 187 let indent = row.indentLevel; 188 if (row.index !== -1) { 189 indent--; 190 } 191 192 if (indent > 0) { 193 renderedRow.push(m('td', {colspan: indent})); 194 } 195 if (row.index !== -1) { 196 renderedRow.push(m('td', {class: 'array-index'}, `[${row.index}]`)); 197 } 198 if (isTableHeader(row.contents)) { 199 renderedRow.push( 200 m('th', 201 {colspan: keyColumnCount + 1 - row.indentLevel}, 202 row.contents.header)); 203 } else { 204 renderedRow.push( 205 m('th', 206 {colspan: keyColumnCount - row.indentLevel}, 207 row.contents.key)); 208 const value = row.contents.value; 209 if (typeof value === 'string') { 210 renderedRow.push(m('td', value)); 211 } else { 212 // Type of value being a record is not propagated into the callback 213 // for some reason, extracting necessary parts as constants instead. 214 const sliceId = value.sliceId; 215 const trackId = value.trackId; 216 renderedRow.push( 217 m('td', 218 m('i.material-icons.grey', 219 { 220 onclick: () => { 221 globals.makeSelection(Actions.selectChromeSlice( 222 {id: sliceId, trackId, table: 'slice'})); 223 // Ideally we want to have a callback to 224 // findCurrentSelection after this selection has been 225 // made. Here we do not have the info for horizontally 226 // scrolling to ts. 227 verticalScrollToTrack(trackId, true); 228 }, 229 title: 'Go to destination slice' 230 }, 231 'call_made'))); 232 } 233 } 234 235 rows.push(m('tr', renderedRow)); 236 } 237 238 return m('table.half-width', rows); 239 } 240 241 fillDescription(description: Map<string, string>, builder: TableBuilder) { 242 for (const [key, value] of description) { 243 builder.add(key, value); 244 } 245 } 246} 247