• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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