blob: 0fa99bb76819f3611b01f48a901ce956683a42cc [file] [log] [blame]
// 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);
}
}
}