| // Copyright (C) 2019 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use size 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 * as m from 'mithril'; |
| |
| import {Actions} from '../common/actions'; |
| import {Arg, ArgsTree, isArgTreeArray, isArgTreeMap} from '../common/arg_types'; |
| import {timeToCode, toNs} from '../common/time'; |
| |
| import {globals, SliceDetails} from './globals'; |
| import {Panel, PanelSize} from './panel'; |
| import {verticalScrollToTrack} from './scroll_helper'; |
| |
| // Table row contents is one of two things: |
| // 1. Key-value pair |
| interface KVPair { |
| kind: 'KVPair'; |
| key: string; |
| value: Arg; |
| } |
| |
| // 2. Common prefix for values in an array |
| interface TableHeader { |
| kind: 'TableHeader'; |
| header: string; |
| } |
| |
| type RowContents = KVPair|TableHeader; |
| |
| function isTableHeader(contents: RowContents): contents is TableHeader { |
| return contents.kind === 'TableHeader'; |
| } |
| |
| interface Row { |
| // How many columns (empty or with an index) precede a key |
| indentLevel: number; |
| // Index if the current row is an element of array |
| index: number; |
| contents: RowContents; |
| } |
| |
| class TableBuilder { |
| // Stack contains indices inside repeated fields, or -1 if the appropriate |
| // index is already displayed. |
| stack: number[] = []; |
| |
| // Row data generated by builder |
| rows: Row[] = []; |
| |
| // Maximum indent level of a key, used to determine total number of columns |
| maxIndent = 0; |
| |
| // Add a key-value pair into the table |
| add(key: string, value: Arg) { |
| this.rows.push( |
| {indentLevel: 0, index: -1, contents: {kind: 'KVPair', key, value}}); |
| } |
| |
| // Add arguments tree into the table |
| addTree(tree: ArgsTree) { |
| this.addTreeInternal(tree, ''); |
| } |
| |
| // Return indent level and index for a fresh row |
| private prepareRow(): [number, number] { |
| const level = this.stack.length; |
| let index = -1; |
| if (level > 0) { |
| index = this.stack[level - 1]; |
| if (index !== -1) { |
| this.stack[level - 1] = -1; |
| } |
| } |
| this.maxIndent = Math.max(this.maxIndent, level); |
| return [level, index]; |
| } |
| |
| private addTreeInternal(record: ArgsTree, prefix: string) { |
| if (isArgTreeArray(record)) { |
| // Add the current prefix as a separate row |
| const row = this.prepareRow(); |
| this.rows.push({ |
| indentLevel: row[0], |
| index: row[1], |
| contents: {kind: 'TableHeader', header: prefix} |
| }); |
| |
| for (let i = 0; i < record.length; i++) { |
| // Push the current array index to the stack. |
| this.stack.push(i); |
| // Prefix is empty for array elements because we don't want to repeat |
| // the common prefix |
| this.addTreeInternal(record[i], ''); |
| this.stack.pop(); |
| } |
| } else if (isArgTreeMap(record)) { |
| for (const [key, value] of Object.entries(record)) { |
| // If the prefix was non-empty, we have to add dot at the end as well. |
| const newPrefix = (prefix === '') ? key : prefix + '.' + key; |
| this.addTreeInternal(value, newPrefix); |
| } |
| } else { |
| // Leaf value in the tree: add to the table |
| const row = this.prepareRow(); |
| this.rows.push({ |
| indentLevel: row[0], |
| index: row[1], |
| contents: {kind: 'KVPair', key: prefix, value: record} |
| }); |
| } |
| } |
| } |
| |
| export class ChromeSliceDetailsPanel extends Panel { |
| view() { |
| const sliceInfo = globals.sliceDetails; |
| if (sliceInfo.ts !== undefined && sliceInfo.dur !== undefined && |
| sliceInfo.name !== undefined) { |
| const builder = new TableBuilder(); |
| builder.add('Name', sliceInfo.name); |
| builder.add( |
| 'Category', |
| !sliceInfo.category || sliceInfo.category === '[NULL]' ? |
| 'N/A' : |
| sliceInfo.category); |
| builder.add('Start time', timeToCode(sliceInfo.ts)); |
| builder.add( |
| 'Duration', |
| toNs(sliceInfo.dur) === -1 ? '-1 (Did not end)' : |
| timeToCode(sliceInfo.dur)); |
| builder.add( |
| 'Slice ID', sliceInfo.id ? sliceInfo.id.toString() : 'Unknown'); |
| if (sliceInfo.description) { |
| this.fillDescription(sliceInfo.description, builder); |
| } |
| this.fillArgs(sliceInfo, builder); |
| return m( |
| '.details-panel', |
| m('.details-panel-heading', m('h2', `Slice Details`)), |
| m('.details-table', this.renderTable(builder))); |
| } else { |
| return m( |
| '.details-panel', |
| m('.details-panel-heading', |
| m( |
| 'h2', |
| `Slice Details`, |
| ))); |
| } |
| } |
| |
| renderCanvas(_ctx: CanvasRenderingContext2D, _size: PanelSize) {} |
| |
| fillArgs(slice: SliceDetails, builder: TableBuilder) { |
| if (slice.argsTree && slice.args) { |
| // Parsed arguments are available, need only to iterate over them to get |
| // slice references |
| for (const [key, value] of slice.args) { |
| if (typeof value !== 'string') { |
| builder.add(key, value); |
| } |
| } |
| builder.addTree(slice.argsTree); |
| } else if (slice.args) { |
| // Parsing has failed, but arguments are available: display them in a flat |
| // 2-column table |
| for (const [key, value] of slice.args) { |
| builder.add(key, value); |
| } |
| } |
| } |
| |
| renderTable(builder: TableBuilder): m.Vnode { |
| const rows: m.Vnode[] = []; |
| const keyColumnCount = builder.maxIndent + 1; |
| for (const row of builder.rows) { |
| const renderedRow: m.Vnode[] = []; |
| let indent = row.indentLevel; |
| if (row.index !== -1) { |
| indent--; |
| } |
| |
| if (indent > 0) { |
| renderedRow.push(m('td', {colspan: indent})); |
| } |
| if (row.index !== -1) { |
| renderedRow.push(m('td', {class: 'array-index'}, `[${row.index}]`)); |
| } |
| if (isTableHeader(row.contents)) { |
| renderedRow.push( |
| m('th', |
| {colspan: keyColumnCount + 1 - row.indentLevel}, |
| row.contents.header)); |
| } else { |
| renderedRow.push( |
| m('th', |
| {colspan: keyColumnCount - row.indentLevel}, |
| row.contents.key)); |
| const value = row.contents.value; |
| if (typeof value === 'string') { |
| renderedRow.push(m('td', value)); |
| } else { |
| // Type of value being a record is not propagated into the callback |
| // for some reason, extracting necessary parts as constants instead. |
| const sliceId = value.sliceId; |
| const trackId = value.trackId; |
| renderedRow.push( |
| m('td', |
| m('i.material-icons.grey', |
| { |
| onclick: () => { |
| globals.makeSelection(Actions.selectChromeSlice( |
| {id: sliceId, trackId, table: 'slice'})); |
| // Ideally we want to have a callback to |
| // findCurrentSelection after this selection has been |
| // made. Here we do not have the info for horizontally |
| // scrolling to ts. |
| verticalScrollToTrack(trackId, true); |
| }, |
| title: 'Go to destination slice' |
| }, |
| 'call_made'))); |
| } |
| } |
| |
| rows.push(m('tr', renderedRow)); |
| } |
| |
| return m('table.half-width.auto-layout', rows); |
| } |
| |
| fillDescription(description: Map<string, string>, builder: TableBuilder) { |
| for (const [key, value] of description) { |
| builder.add(key, value); |
| } |
| } |
| } |