1// Copyright (C) 2024 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 {time, Time} from '../../base/time'; 17import {DetailsShell} from '../../widgets/details_shell'; 18import { 19 MultiSelectDiff, 20 MultiSelectOption, 21 PopupMultiSelect, 22} from '../../widgets/multiselect'; 23import {PopupPosition} from '../../widgets/popup'; 24import {Timestamp} from '../../components/widgets/timestamp'; 25import {FtraceFilter, FtraceStat} from './common'; 26import {Engine} from '../../trace_processor/engine'; 27import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result'; 28import {AsyncLimiter} from '../../base/async_limiter'; 29import {Monitor} from '../../base/monitor'; 30import {Button} from '../../widgets/button'; 31import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table'; 32import {Store} from '../../base/store'; 33import {Trace} from '../../public/trace'; 34import {materialColorScheme} from '../../components/colorizer'; 35 36const ROW_H = 20; 37 38interface FtraceExplorerAttrs { 39 cache: FtraceExplorerCache; 40 filterStore: Store<FtraceFilter>; 41 trace: Trace; 42} 43 44interface FtraceEvent { 45 id: number; 46 ts: time; 47 name: string; 48 cpu: number; 49 thread: string | null; 50 process: string | null; 51 args: string; 52} 53 54interface FtracePanelData { 55 events: FtraceEvent[]; 56 offset: number; 57 numEvents: number; // Number of events in the visible window 58} 59 60interface Pagination { 61 offset: number; 62 count: number; 63} 64 65export interface FtraceExplorerCache { 66 state: 'blank' | 'loading' | 'valid'; 67 counters: FtraceStat[]; 68} 69 70async function getFtraceCounters(engine: Engine): Promise<FtraceStat[]> { 71 // TODO(stevegolton): this is an extraordinarily slow query on large traces 72 // as it goes through every ftrace event which can be a lot on big traces. 73 // Consider if we can have some different UX which avoids needing these 74 // counts 75 // TODO(mayzner): the +name below is an awful hack to workaround 76 // extraordinarily slow sorting of strings. However, even with this hack, 77 // this is just a slow query. There are various ways we can improve this 78 // (e.g. with using the vtab_distinct APIs of SQLite). 79 const result = await engine.query(` 80 select 81 name, 82 count(1) as cnt 83 from ftrace_event 84 group by name 85 order by cnt desc 86 `); 87 const counters: FtraceStat[] = []; 88 const it = result.iter({name: STR, cnt: NUM}); 89 for (let row = 0; it.valid(); it.next(), row++) { 90 counters.push({name: it.name, count: it.cnt}); 91 } 92 return counters; 93} 94 95export class FtraceExplorer implements m.ClassComponent<FtraceExplorerAttrs> { 96 private pagination: Pagination = { 97 offset: 0, 98 count: 0, 99 }; 100 private readonly monitor: Monitor; 101 private readonly queryLimiter = new AsyncLimiter(); 102 103 // A cache of the data we have most recently loaded from our store 104 private data?: FtracePanelData; 105 106 constructor({attrs}: m.CVnode<FtraceExplorerAttrs>) { 107 this.monitor = new Monitor([ 108 () => attrs.trace.timeline.visibleWindow.toTimeSpan().start, 109 () => attrs.trace.timeline.visibleWindow.toTimeSpan().end, 110 () => attrs.filterStore.state, 111 ]); 112 113 if (attrs.cache.state === 'blank') { 114 getFtraceCounters(attrs.trace.engine) 115 .then((counters) => { 116 attrs.cache.counters = counters; 117 attrs.cache.state = 'valid'; 118 }) 119 .catch(() => { 120 attrs.cache.state = 'blank'; 121 }); 122 attrs.cache.state = 'loading'; 123 } 124 } 125 126 view({attrs}: m.CVnode<FtraceExplorerAttrs>) { 127 this.monitor.ifStateChanged(() => { 128 this.reloadData(attrs); 129 }); 130 131 return m( 132 DetailsShell, 133 { 134 title: this.renderTitle(), 135 buttons: this.renderFilterPanel(attrs), 136 fillParent: true, 137 }, 138 m(VirtualTable, { 139 className: 'pf-ftrace-explorer', 140 columns: [ 141 {header: 'ID', width: '5em'}, 142 {header: 'Timestamp', width: '13em'}, 143 {header: 'Name', width: '24em'}, 144 {header: 'CPU', width: '3em'}, 145 {header: 'Process', width: '24em'}, 146 {header: 'Args', width: '200em'}, 147 ], 148 firstRowOffset: this.data?.offset ?? 0, 149 numRows: this.data?.numEvents ?? 0, 150 rowHeight: ROW_H, 151 rows: this.renderData(), 152 onReload: (offset, count) => { 153 this.pagination = {offset, count}; 154 this.reloadData(attrs); 155 }, 156 onRowHover: (id) => { 157 const event = this.data?.events.find((event) => event.id === id); 158 if (event) { 159 attrs.trace.timeline.hoverCursorTimestamp = event.ts; 160 } 161 }, 162 onRowOut: () => { 163 attrs.trace.timeline.hoverCursorTimestamp = undefined; 164 }, 165 }), 166 ); 167 } 168 169 private reloadData(attrs: FtraceExplorerAttrs): void { 170 this.queryLimiter.schedule(async () => { 171 this.data = await lookupFtraceEvents( 172 attrs.trace, 173 this.pagination.offset, 174 this.pagination.count, 175 attrs.filterStore.state, 176 ); 177 }); 178 } 179 180 private renderData(): VirtualTableRow[] { 181 if (!this.data) { 182 return []; 183 } 184 185 return this.data.events.map((event) => { 186 const {ts, name, cpu, process, args, id} = event; 187 const timestamp = m(Timestamp, {ts}); 188 const color = materialColorScheme(name).base.cssString; 189 190 return { 191 id, 192 cells: [ 193 id, 194 timestamp, 195 m( 196 '.pf-ftrace-namebox', 197 m('.pf-ftrace-colorbox', {style: {background: color}}), 198 name, 199 ), 200 cpu, 201 process, 202 args, 203 ], 204 }; 205 }); 206 } 207 208 private renderTitle() { 209 if (this.data) { 210 const {numEvents} = this.data; 211 return `Ftrace Events (${numEvents})`; 212 } else { 213 return 'Ftrace Events'; 214 } 215 } 216 217 private renderFilterPanel(attrs: FtraceExplorerAttrs) { 218 if (attrs.cache.state !== 'valid') { 219 return m(Button, { 220 label: 'Filter', 221 disabled: true, 222 loading: true, 223 }); 224 } 225 226 const excludeList = attrs.filterStore.state.excludeList; 227 const options: MultiSelectOption[] = attrs.cache.counters.map( 228 ({name, count}) => { 229 return { 230 id: name, 231 name: `${name} (${count})`, 232 checked: !excludeList.some((excluded: string) => excluded === name), 233 }; 234 }, 235 ); 236 237 return m(PopupMultiSelect, { 238 label: 'Filter', 239 icon: 'filter_list_alt', 240 popupPosition: PopupPosition.Top, 241 options, 242 onChange: (diffs: MultiSelectDiff[]) => { 243 const newList = new Set<string>(excludeList); 244 diffs.forEach(({checked, id}) => { 245 if (checked) { 246 newList.delete(id); 247 } else { 248 newList.add(id); 249 } 250 }); 251 attrs.filterStore.edit((draft) => { 252 draft.excludeList = Array.from(newList); 253 }); 254 }, 255 }); 256 } 257} 258 259async function lookupFtraceEvents( 260 trace: Trace, 261 offset: number, 262 count: number, 263 filter: FtraceFilter, 264): Promise<FtracePanelData> { 265 const {start, end} = trace.timeline.visibleWindow.toTimeSpan(); 266 267 const excludeList = filter.excludeList; 268 const excludeListSql = excludeList.map((s) => `'${s}'`).join(','); 269 270 // TODO(stevegolton): This query can be slow when traces are huge. 271 // The number of events is only used for correctly sizing the panel's 272 // scroll container so that the scrollbar works as if the panel were fully 273 // populated. 274 // Perhaps we could work out some UX that doesn't need this. 275 let queryRes = await trace.engine.query(` 276 select count(id) as numEvents 277 from ftrace_event 278 where 279 ftrace_event.name not in (${excludeListSql}) and 280 ts >= ${start} and ts <= ${end} 281 `); 282 const {numEvents} = queryRes.firstRow({numEvents: NUM}); 283 284 queryRes = await trace.engine.query(` 285 select 286 ftrace_event.id as id, 287 ftrace_event.ts as ts, 288 ftrace_event.name as name, 289 ftrace_event.cpu as cpu, 290 thread.name as thread, 291 process.name as process, 292 to_ftrace(ftrace_event.id) as args 293 from ftrace_event 294 join thread using (utid) 295 left join process on thread.upid = process.upid 296 where 297 ftrace_event.name not in (${excludeListSql}) and 298 ts >= ${start} and ts <= ${end} 299 order by id 300 limit ${count} offset ${offset};`); 301 const events: FtraceEvent[] = []; 302 const it = queryRes.iter({ 303 id: NUM, 304 ts: LONG, 305 name: STR, 306 cpu: NUM, 307 thread: STR_NULL, 308 process: STR_NULL, 309 args: STR, 310 }); 311 for (let row = 0; it.valid(); it.next(), row++) { 312 events.push({ 313 id: it.id, 314 ts: Time.fromRaw(it.ts), 315 name: it.name, 316 cpu: it.cpu, 317 thread: it.thread, 318 process: it.process, 319 args: it.args, 320 }); 321 } 322 return {events, offset, numEvents}; 323} 324