// Copyright (C) 2020 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 m from 'mithril'; import {Icons} from '../base/semantic_icons'; import {Actions} from '../common/actions'; import {getLegacySelection} from '../common/state'; import {raf} from '../core/raf_scheduler'; import {Flow, globals} from './globals'; import {DurationWidget} from './widgets/duration'; import {EmptyState} from '../widgets/empty_state'; export const ALL_CATEGORIES = '_all_'; export function getFlowCategories(flow: Flow): string[] { const categories: string[] = []; // v1 flows have their own categories if (flow.category) { categories.push(...flow.category.split(',')); return categories; } const beginCats = flow.begin.sliceCategory.split(','); const endCats = flow.end.sliceCategory.split(','); categories.push(...new Set([...beginCats, ...endCats])); return categories; } export class FlowEventsPanel implements m.ClassComponent { view() { const selection = getLegacySelection(globals.state); if (!selection) { return m( EmptyState, { className: 'pf-noselection', title: 'Nothing selected', }, 'Flow data will appear here', ); } if (selection.kind !== 'SLICE') { return m( EmptyState, { className: 'pf-noselection', title: 'No flow data', icon: 'warning', }, `Flows are not applicable to the selection kind: '${selection.kind}'`, ); } const flowClickHandler = (sliceId: number, trackId: number) => { const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId); if (trackKey) { globals.setLegacySelection( { kind: 'SLICE', id: sliceId, trackKey, table: 'slice', }, { clearSearch: true, pendingScrollId: undefined, switchToCurrentSelectionTab: false, }, ); } }; // Can happen only for flow events version 1 const haveCategories = globals.connectedFlows.filter((flow) => flow.category).length > 0; const columns = [ m('th', 'Direction'), m('th', 'Duration'), m('th', 'Connected Slice ID'), m('th', 'Connected Slice Name'), m('th', 'Thread Out'), m('th', 'Thread In'), m('th', 'Process Out'), m('th', 'Process In'), ]; if (haveCategories) { columns.push(m('th', 'Flow Category')); columns.push(m('th', 'Flow Name')); } const rows = [m('tr', columns)]; // Fill the table with all the directly connected flow events globals.connectedFlows.forEach((flow) => { if ( selection.id !== flow.begin.sliceId && selection.id !== flow.end.sliceId ) { return; } const outgoing = selection.id === flow.begin.sliceId; const otherEnd = outgoing ? flow.end : flow.begin; const args = { onclick: () => flowClickHandler(otherEnd.sliceId, otherEnd.trackId), onmousemove: () => globals.dispatch( Actions.setHighlightedSliceId({sliceId: otherEnd.sliceId}), ), onmouseleave: () => globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1})), }; const data = [ m('td.flow-link', args, outgoing ? 'Outgoing' : 'Incoming'), m('td.flow-link', args, m(DurationWidget, {dur: flow.dur})), m('td.flow-link', args, otherEnd.sliceId.toString()), m('td.flow-link', args, otherEnd.sliceName), m('td.flow-link', args, flow.begin.threadName), m('td.flow-link', args, flow.end.threadName), m('td.flow-link', args, flow.begin.processName), m('td.flow-link', args, flow.end.processName), ]; if (haveCategories) { data.push(m('td.flow-info', flow.category || '-')); data.push(m('td.flow-info', flow.name || '-')); } rows.push(m('tr', data)); }); return m('.details-panel', [ m('.details-panel-heading', m('h2', `Flow events`)), m('.flow-events-table', m('table', rows)), ]); } } export class FlowEventsAreaSelectedPanel implements m.ClassComponent { view() { const selection = globals.state.selection; if (selection.kind !== 'area') { return; } const columns = [ m('th', 'Flow Category'), m('th', 'Number of flows'), m( 'th', 'Show', m( 'a.warning', m('i.material-icons', 'warning'), m( '.tooltip', 'Showing a large number of flows may impact performance.', ), ), ), ]; const rows = [m('tr', columns)]; const categoryToFlowsNum = new Map(); globals.selectedFlows.forEach((flow) => { const categories = getFlowCategories(flow); categories.forEach((cat) => { if (!categoryToFlowsNum.has(cat)) { categoryToFlowsNum.set(cat, 0); } categoryToFlowsNum.set(cat, categoryToFlowsNum.get(cat)! + 1); }); }); const allWasChecked = globals.visibleFlowCategories.get(ALL_CATEGORIES); rows.push( m('tr.sum', [ m('td.sum-data', 'All'), m('td.sum-data', globals.selectedFlows.length), m( 'td.sum-data', m( 'i.material-icons', { onclick: () => { if (allWasChecked) { globals.visibleFlowCategories.clear(); } else { categoryToFlowsNum.forEach((_, cat) => { globals.visibleFlowCategories.set(cat, true); }); } globals.visibleFlowCategories.set( ALL_CATEGORIES, !allWasChecked, ); raf.scheduleFullRedraw(); }, }, allWasChecked ? Icons.Checkbox : Icons.BlankCheckbox, ), ), ]), ); categoryToFlowsNum.forEach((num, cat) => { const wasChecked = globals.visibleFlowCategories.get(cat) || globals.visibleFlowCategories.get(ALL_CATEGORIES); const data = [ m('td.flow-info', cat), m('td.flow-info', num), m( 'td.flow-info', m( 'i.material-icons', { onclick: () => { if (wasChecked) { globals.visibleFlowCategories.set(ALL_CATEGORIES, false); } globals.visibleFlowCategories.set(cat, !wasChecked); raf.scheduleFullRedraw(); }, }, wasChecked ? Icons.Checkbox : Icons.BlankCheckbox, ), ), ]; rows.push(m('tr', data)); }); return m('.details-panel', [ m('.details-panel-heading', m('h2', `Selected flow events`)), m('.flow-events-table', m('table', rows)), ]); } }