1// Copyright (C) 2020 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 {Icons} from '../base/semantic_icons'; 18import {Actions} from '../common/actions'; 19import {getLegacySelection} from '../common/state'; 20import {raf} from '../core/raf_scheduler'; 21 22import {Flow, globals} from './globals'; 23import {DurationWidget} from './widgets/duration'; 24import {EmptyState} from '../widgets/empty_state'; 25 26export const ALL_CATEGORIES = '_all_'; 27 28export function getFlowCategories(flow: Flow): string[] { 29 const categories: string[] = []; 30 // v1 flows have their own categories 31 if (flow.category) { 32 categories.push(...flow.category.split(',')); 33 return categories; 34 } 35 const beginCats = flow.begin.sliceCategory.split(','); 36 const endCats = flow.end.sliceCategory.split(','); 37 categories.push(...new Set([...beginCats, ...endCats])); 38 return categories; 39} 40 41export class FlowEventsPanel implements m.ClassComponent { 42 view() { 43 const selection = getLegacySelection(globals.state); 44 if (!selection) { 45 return m( 46 EmptyState, 47 { 48 className: 'pf-noselection', 49 title: 'Nothing selected', 50 }, 51 'Flow data will appear here', 52 ); 53 } 54 55 if (selection.kind !== 'SLICE') { 56 return m( 57 EmptyState, 58 { 59 className: 'pf-noselection', 60 title: 'No flow data', 61 icon: 'warning', 62 }, 63 `Flows are not applicable to the selection kind: '${selection.kind}'`, 64 ); 65 } 66 67 const flowClickHandler = (sliceId: number, trackId: number) => { 68 const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId); 69 if (trackKey) { 70 globals.setLegacySelection( 71 { 72 kind: 'SLICE', 73 id: sliceId, 74 trackKey, 75 table: 'slice', 76 }, 77 { 78 clearSearch: true, 79 pendingScrollId: undefined, 80 switchToCurrentSelectionTab: false, 81 }, 82 ); 83 } 84 }; 85 86 // Can happen only for flow events version 1 87 const haveCategories = 88 globals.connectedFlows.filter((flow) => flow.category).length > 0; 89 90 const columns = [ 91 m('th', 'Direction'), 92 m('th', 'Duration'), 93 m('th', 'Connected Slice ID'), 94 m('th', 'Connected Slice Name'), 95 m('th', 'Thread Out'), 96 m('th', 'Thread In'), 97 m('th', 'Process Out'), 98 m('th', 'Process In'), 99 ]; 100 101 if (haveCategories) { 102 columns.push(m('th', 'Flow Category')); 103 columns.push(m('th', 'Flow Name')); 104 } 105 106 const rows = [m('tr', columns)]; 107 108 // Fill the table with all the directly connected flow events 109 globals.connectedFlows.forEach((flow) => { 110 if ( 111 selection.id !== flow.begin.sliceId && 112 selection.id !== flow.end.sliceId 113 ) { 114 return; 115 } 116 117 const outgoing = selection.id === flow.begin.sliceId; 118 const otherEnd = outgoing ? flow.end : flow.begin; 119 120 const args = { 121 onclick: () => flowClickHandler(otherEnd.sliceId, otherEnd.trackId), 122 onmousemove: () => 123 globals.dispatch( 124 Actions.setHighlightedSliceId({sliceId: otherEnd.sliceId}), 125 ), 126 onmouseleave: () => 127 globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1})), 128 }; 129 130 const data = [ 131 m('td.flow-link', args, outgoing ? 'Outgoing' : 'Incoming'), 132 m('td.flow-link', args, m(DurationWidget, {dur: flow.dur})), 133 m('td.flow-link', args, otherEnd.sliceId.toString()), 134 m('td.flow-link', args, otherEnd.sliceName), 135 m('td.flow-link', args, flow.begin.threadName), 136 m('td.flow-link', args, flow.end.threadName), 137 m('td.flow-link', args, flow.begin.processName), 138 m('td.flow-link', args, flow.end.processName), 139 ]; 140 141 if (haveCategories) { 142 data.push(m('td.flow-info', flow.category || '-')); 143 data.push(m('td.flow-info', flow.name || '-')); 144 } 145 146 rows.push(m('tr', data)); 147 }); 148 149 return m('.details-panel', [ 150 m('.details-panel-heading', m('h2', `Flow events`)), 151 m('.flow-events-table', m('table', rows)), 152 ]); 153 } 154} 155 156export class FlowEventsAreaSelectedPanel implements m.ClassComponent { 157 view() { 158 const selection = globals.state.selection; 159 if (selection.kind !== 'area') { 160 return; 161 } 162 163 const columns = [ 164 m('th', 'Flow Category'), 165 m('th', 'Number of flows'), 166 m( 167 'th', 168 'Show', 169 m( 170 'a.warning', 171 m('i.material-icons', 'warning'), 172 m( 173 '.tooltip', 174 'Showing a large number of flows may impact performance.', 175 ), 176 ), 177 ), 178 ]; 179 180 const rows = [m('tr', columns)]; 181 182 const categoryToFlowsNum = new Map<string, number>(); 183 184 globals.selectedFlows.forEach((flow) => { 185 const categories = getFlowCategories(flow); 186 categories.forEach((cat) => { 187 if (!categoryToFlowsNum.has(cat)) { 188 categoryToFlowsNum.set(cat, 0); 189 } 190 categoryToFlowsNum.set(cat, categoryToFlowsNum.get(cat)! + 1); 191 }); 192 }); 193 194 const allWasChecked = globals.visibleFlowCategories.get(ALL_CATEGORIES); 195 rows.push( 196 m('tr.sum', [ 197 m('td.sum-data', 'All'), 198 m('td.sum-data', globals.selectedFlows.length), 199 m( 200 'td.sum-data', 201 m( 202 'i.material-icons', 203 { 204 onclick: () => { 205 if (allWasChecked) { 206 globals.visibleFlowCategories.clear(); 207 } else { 208 categoryToFlowsNum.forEach((_, cat) => { 209 globals.visibleFlowCategories.set(cat, true); 210 }); 211 } 212 globals.visibleFlowCategories.set( 213 ALL_CATEGORIES, 214 !allWasChecked, 215 ); 216 raf.scheduleFullRedraw(); 217 }, 218 }, 219 allWasChecked ? Icons.Checkbox : Icons.BlankCheckbox, 220 ), 221 ), 222 ]), 223 ); 224 225 categoryToFlowsNum.forEach((num, cat) => { 226 const wasChecked = 227 globals.visibleFlowCategories.get(cat) || 228 globals.visibleFlowCategories.get(ALL_CATEGORIES); 229 const data = [ 230 m('td.flow-info', cat), 231 m('td.flow-info', num), 232 m( 233 'td.flow-info', 234 m( 235 'i.material-icons', 236 { 237 onclick: () => { 238 if (wasChecked) { 239 globals.visibleFlowCategories.set(ALL_CATEGORIES, false); 240 } 241 globals.visibleFlowCategories.set(cat, !wasChecked); 242 raf.scheduleFullRedraw(); 243 }, 244 }, 245 wasChecked ? Icons.Checkbox : Icons.BlankCheckbox, 246 ), 247 ), 248 ]; 249 rows.push(m('tr', data)); 250 }); 251 252 return m('.details-panel', [ 253 m('.details-panel-heading', m('h2', `Selected flow events`)), 254 m('.flow-events-table', m('table', rows)), 255 ]); 256 } 257} 258