1// Copyright (C) 2023 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this 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'; 16import {StringListPatch} from 'src/common/state'; 17 18import {assertExists} from '../base/logging'; 19import {Actions} from '../common/actions'; 20import {colorForString} from '../common/colorizer'; 21import {formatTPTime, TPTime} from '../common/time'; 22 23import {globals} from './globals'; 24import {Panel} from './panel'; 25import { 26 MultiSelect, 27 MultiSelectDiff, 28 Option as MultiSelectOption, 29} from './widgets/multiselect'; 30import {PopupPosition} from './widgets/popup'; 31 32const ROW_H = 20; 33const PAGE_SIZE = 250; 34 35// This class is quite a weird one. The state looks something like this: 36// 37// view() -> renders the panel from the data, for now we have no idea what size 38// the scroll window is going to be so we don't know how many rows to ask for, 39// and the number of rendered rows in our state is likely going to be 0 or wrong 40// 41// oncreate() -> we now know how many rows we need to display and our scroll 42// offset. This is where we as our controller to update the rows, which could 43// take some time. Record the first and last row we can see. Attach scroll 44// handler to the scrolly window here. 45// 46// onScroll() -> we know the window has been scrolled, we need to see if things 47// have changed enough to constitute a redraw. 48 49// Another call to view() can come at any time, as a reusult of the controller 50// giving us some data. 51// 52export class FtracePanel extends Panel<{}> { 53 private page: number = 0; 54 private pageCount: number = 0; 55 56 view(_: m.CVnode<{}>) { 57 return m( 58 '.ftrace-panel', 59 m( 60 '.sticky', 61 [ 62 this.renderRowsLabel(), 63 this.renderFilterPanel(), 64 ], 65 ), 66 this.renderRows(), 67 ); 68 } 69 70 private scrollContainer(dom: Element): HTMLElement { 71 const el = dom.parentElement!.parentElement!.parentElement; 72 return assertExists(el); 73 } 74 75 oncreate({dom}: m.CVnodeDOM) { 76 const sc = this.scrollContainer(dom); 77 sc.addEventListener('scroll', this.onScroll); 78 this.recomputeVisibleRowsAndUpdate(sc); 79 } 80 81 onupdate({dom}: m.CVnodeDOM) { 82 const sc = this.scrollContainer(dom); 83 this.recomputeVisibleRowsAndUpdate(sc); 84 } 85 86 recomputeVisibleRowsAndUpdate(scrollContainer: HTMLElement) { 87 const prevPage = this.page; 88 const prevPageCount = this.pageCount; 89 90 const visibleRowOffset = Math.floor(scrollContainer.scrollTop / ROW_H); 91 const visibleRowCount = Math.ceil(scrollContainer.clientHeight / ROW_H); 92 93 // Work out which "page" we're on 94 this.page = Math.floor(visibleRowOffset / PAGE_SIZE) - 1; 95 this.pageCount = Math.ceil(visibleRowCount / PAGE_SIZE) + 2; 96 97 if (this.page !== prevPage || this.pageCount !== prevPageCount) { 98 globals.dispatch(Actions.updateFtracePagination({ 99 offset: Math.max(0, this.page) * PAGE_SIZE, 100 count: this.pageCount * PAGE_SIZE, 101 })); 102 } 103 } 104 105 onremove({dom}: m.CVnodeDOM) { 106 const sc = this.scrollContainer(dom); 107 sc.removeEventListener('scroll', this.onScroll); 108 109 globals.dispatch(Actions.updateFtracePagination({ 110 offset: 0, 111 count: 0, 112 })); 113 } 114 115 onScroll = (e: Event) => { 116 const scrollContainer = e.target as HTMLElement; 117 this.recomputeVisibleRowsAndUpdate(scrollContainer); 118 }; 119 120 onRowOver(ts: TPTime) { 121 globals.dispatch(Actions.setHoverCursorTimestamp({ts})); 122 } 123 124 onRowOut() { 125 globals.dispatch(Actions.setHoverCursorTimestamp({ts: -1n})); 126 } 127 128 private renderRowsLabel() { 129 if (globals.ftracePanelData) { 130 const {numEvents} = globals.ftracePanelData; 131 return m('.ftrace-rows-label', `Ftrace Events (${numEvents})`); 132 } else { 133 return m('.ftrace-rows-label', 'Ftrace Rows'); 134 } 135 } 136 137 private renderFilterPanel() { 138 if (!globals.ftraceCounters) { 139 return null; 140 } 141 142 const options: MultiSelectOption[] = 143 globals.ftraceCounters.map(({name, count}) => { 144 return { 145 id: name, 146 name: `${name} (${count})`, 147 checked: !globals.state.ftraceFilter.excludedNames.some( 148 (excluded: string) => excluded === name), 149 }; 150 }); 151 152 return m( 153 MultiSelect, 154 { 155 label: 'Filter by name', 156 icon: 'filter_list_alt', 157 popupPosition: PopupPosition.Top, 158 options, 159 onChange: (diffs: MultiSelectDiff[]) => { 160 const excludedNames: StringListPatch[] = diffs.map( 161 ({id, checked}) => [checked ? 'remove' : 'add', id], 162 ); 163 globals.dispatchMultiple([ 164 Actions.updateFtraceFilter({excludedNames}), 165 Actions.requestTrackReload({}), 166 ]); 167 }, 168 }, 169 ); 170 } 171 172 // Render all the rows including the first title row 173 private renderRows() { 174 const data = globals.ftracePanelData; 175 const rows: m.Children = []; 176 177 rows.push(m( 178 `.row`, 179 m('.cell.row-header', 'Timestamp'), 180 m('.cell.row-header', 'Name'), 181 m('.cell.row-header', 'CPU'), 182 m('.cell.row-header', 'Process'), 183 m('.cell.row-header', 'Args'), 184 )); 185 186 if (data) { 187 const {events, offset, numEvents} = data; 188 for (let i = 0; i < events.length; i++) { 189 const {ts, name, cpu, process, args} = events[i]; 190 191 const timestamp = formatTPTime(ts - globals.state.traceTime.start); 192 193 const rank = i + offset; 194 195 const color = colorForString(name); 196 const hsl = `hsl( 197 ${color.h}, 198 ${color.s - 20}%, 199 ${Math.min(color.l + 10, 60)}% 200 )`; 201 202 rows.push(m( 203 `.row`, 204 { 205 style: {top: `${(rank + 1.0) * ROW_H}px`}, 206 onmouseover: this.onRowOver.bind(this, ts), 207 onmouseout: this.onRowOut.bind(this), 208 }, 209 m('.cell', timestamp), 210 m('.cell', m('span.colour', {style: {background: hsl}}), name), 211 m('.cell', cpu), 212 m('.cell', process), 213 m('.cell', args), 214 )); 215 } 216 return m('.rows', {style: {height: `${numEvents * ROW_H}px`}}, rows); 217 } else { 218 return m('.rows', rows); 219 } 220 } 221 222 renderCanvas() {} 223} 224