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 m from 'mithril'; 16 17import {Actions} from '../common/actions'; 18import { 19 AggregateData, 20 Column, 21 ThreadStateExtra, 22 isEmptyData, 23} from '../common/aggregation_data'; 24import {colorForState} from '../core/colorizer'; 25import {translateState} from '../common/thread_state'; 26 27import {globals} from './globals'; 28import {DurationWidget} from './widgets/duration'; 29import {EmptyState} from '../widgets/empty_state'; 30import {Anchor} from '../widgets/anchor'; 31import {Icons} from '../base/semantic_icons'; 32 33export interface AggregationPanelAttrs { 34 data?: AggregateData; 35 kind: string; 36} 37 38export class AggregationPanel 39 implements m.ClassComponent<AggregationPanelAttrs> 40{ 41 view({attrs}: m.CVnode<AggregationPanelAttrs>) { 42 if (!attrs.data || isEmptyData(attrs.data)) { 43 return m( 44 EmptyState, 45 { 46 className: 'pf-noselection', 47 title: 'No relevant tracks in selection', 48 }, 49 m( 50 Anchor, 51 { 52 icon: Icons.ChangeTab, 53 onclick: () => { 54 globals.dispatch(Actions.showTab({uri: 'current_selection'})); 55 }, 56 }, 57 'Go to current selection tab', 58 ), 59 ); 60 } 61 62 return m( 63 '.details-panel', 64 m( 65 '.details-panel-heading.aggregation', 66 attrs.data.extra !== undefined && 67 attrs.data.extra.kind === 'THREAD_STATE' 68 ? this.showStateSummary(attrs.data.extra) 69 : null, 70 this.showTimeRange(), 71 m( 72 'table', 73 m( 74 'tr', 75 attrs.data.columns.map((col) => 76 this.formatColumnHeading(col, attrs.kind), 77 ), 78 ), 79 m( 80 'tr.sum', 81 attrs.data.columnSums.map((sum) => { 82 const sumClass = sum === '' ? 'td' : 'td.sum-data'; 83 return m(sumClass, sum); 84 }), 85 ), 86 ), 87 ), 88 m('.details-table.aggregation', m('table', this.getRows(attrs.data))), 89 ); 90 } 91 92 formatColumnHeading(col: Column, id: string) { 93 const pref = globals.state.aggregatePreferences[id]; 94 let sortIcon = ''; 95 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 96 if (pref && pref.sorting && pref.sorting.column === col.columnId) { 97 sortIcon = 98 pref.sorting.direction === 'DESC' ? 'arrow_drop_down' : 'arrow_drop_up'; 99 } 100 return m( 101 'th', 102 { 103 onclick: () => { 104 globals.dispatch( 105 Actions.updateAggregateSorting({id, column: col.columnId}), 106 ); 107 }, 108 }, 109 col.title, 110 m('i.material-icons', sortIcon), 111 ); 112 } 113 114 getRows(data: AggregateData) { 115 if (data.columns.length === 0) return; 116 const rows = []; 117 for (let i = 0; i < data.columns[0].data.length; i++) { 118 const row = []; 119 for (let j = 0; j < data.columns.length; j++) { 120 row.push(m('td', this.getFormattedData(data, i, j))); 121 } 122 rows.push(m('tr', row)); 123 } 124 return rows; 125 } 126 127 getFormattedData(data: AggregateData, rowIndex: number, columnIndex: number) { 128 switch (data.columns[columnIndex].kind) { 129 case 'STRING': 130 return data.strings[data.columns[columnIndex].data[rowIndex]]; 131 case 'TIMESTAMP_NS': 132 return `${data.columns[columnIndex].data[rowIndex] / 1000000}`; 133 case 'STATE': { 134 const concatState = 135 data.strings[data.columns[columnIndex].data[rowIndex]]; 136 const split = concatState.split(','); 137 const ioWait = 138 split[1] === 'NULL' ? undefined : !!Number.parseInt(split[1], 10); 139 return translateState(split[0], ioWait); 140 } 141 case 'NUMBER': 142 default: 143 return data.columns[columnIndex].data[rowIndex]; 144 } 145 } 146 147 showTimeRange() { 148 const selection = globals.state.selection; 149 if (selection.kind !== 'area') return undefined; 150 const duration = selection.end - selection.start; 151 return m( 152 '.time-range', 153 'Selected range: ', 154 m(DurationWidget, {dur: duration}), 155 ); 156 } 157 158 // Thread state aggregation panel only 159 showStateSummary(data: ThreadStateExtra) { 160 if (data === undefined) return undefined; 161 const states = []; 162 for (let i = 0; i < data.states.length; i++) { 163 const colorScheme = colorForState(data.states[i]); 164 const width = (data.values[i] / data.totalMs) * 100; 165 states.push( 166 m( 167 '.state', 168 { 169 style: { 170 background: colorScheme.base.cssString, 171 color: colorScheme.textBase.cssString, 172 width: `${width}%`, 173 }, 174 }, 175 `${data.states[i]}: ${data.values[i]} ms`, 176 ), 177 ); 178 } 179 return m('.states', states); 180 } 181} 182