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 {colorForFtrace} from '../../components/colorizer'; 18import {DetailsShell} from '../../widgets/details_shell'; 19import { 20 MultiSelectDiff, 21 Option as MultiSelectOption, 22 PopupMultiSelect, 23} from '../../widgets/multiselect'; 24import {PopupPosition} from '../../widgets/popup'; 25import {Timestamp} from '../../components/widgets/timestamp'; 26import {FtraceFilter, FtraceStat} from './common'; 27import {Engine} from '../../trace_processor/engine'; 28import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result'; 29import {AsyncLimiter} from '../../base/async_limiter'; 30import {Monitor} from '../../base/monitor'; 31import {Button} from '../../widgets/button'; 32import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table'; 33import {Store} from '../../base/store'; 34import {Trace} from '../../public/trace'; 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 attrs.trace.scheduleFullRedraw(); 178 }); 179 } 180 181 private renderData(): VirtualTableRow[] { 182 if (!this.data) { 183 return []; 184 } 185 186 return this.data.events.map((event) => { 187 const {ts, name, cpu, process, args, id} = event; 188 const timestamp = m(Timestamp, {ts}); 189 const color = colorForFtrace(name).base.cssString; 190 191 return { 192 id, 193 cells: [ 194 id, 195 timestamp, 196 m( 197 '.pf-ftrace-namebox', 198 m('.pf-ftrace-colorbox', {style: {background: color}}), 199 name, 200 ), 201 cpu, 202 process, 203 args, 204 ], 205 }; 206 }); 207 } 208 209 private renderTitle() { 210 if (this.data) { 211 const {numEvents} = this.data; 212 return `Ftrace Events (${numEvents})`; 213 } else { 214 return 'Ftrace Events'; 215 } 216 } 217 218 private renderFilterPanel(attrs: FtraceExplorerAttrs) { 219 if (attrs.cache.state !== 'valid') { 220 return m(Button, { 221 label: 'Filter', 222 disabled: true, 223 loading: true, 224 }); 225 } 226 227 const excludeList = attrs.filterStore.state.excludeList; 228 const options: MultiSelectOption[] = attrs.cache.counters.map( 229 ({name, count}) => { 230 return { 231 id: name, 232 name: `${name} (${count})`, 233 checked: !excludeList.some((excluded: string) => excluded === name), 234 }; 235 }, 236 ); 237 238 return m(PopupMultiSelect, { 239 label: 'Filter', 240 icon: 'filter_list_alt', 241 popupPosition: PopupPosition.Top, 242 options, 243 onChange: (diffs: MultiSelectDiff[]) => { 244 const newList = new Set<string>(excludeList); 245 diffs.forEach(({checked, id}) => { 246 if (checked) { 247 newList.delete(id); 248 } else { 249 newList.add(id); 250 } 251 }); 252 attrs.filterStore.edit((draft) => { 253 draft.excludeList = Array.from(newList); 254 }); 255 }, 256 }); 257 } 258} 259 260async function lookupFtraceEvents( 261 trace: Trace, 262 offset: number, 263 count: number, 264 filter: FtraceFilter, 265): Promise<FtracePanelData> { 266 const {start, end} = trace.timeline.visibleWindow.toTimeSpan(); 267 268 const excludeList = filter.excludeList; 269 const excludeListSql = excludeList.map((s) => `'${s}'`).join(','); 270 271 // TODO(stevegolton): This query can be slow when traces are huge. 272 // The number of events is only used for correctly sizing the panel's 273 // scroll container so that the scrollbar works as if the panel were fully 274 // populated. 275 // Perhaps we could work out some UX that doesn't need this. 276 let queryRes = await trace.engine.query(` 277 select count(id) as numEvents 278 from ftrace_event 279 where 280 ftrace_event.name not in (${excludeListSql}) and 281 ts >= ${start} and ts <= ${end} 282 `); 283 const {numEvents} = queryRes.firstRow({numEvents: NUM}); 284 285 queryRes = await trace.engine.query(` 286 select 287 ftrace_event.id as id, 288 ftrace_event.ts as ts, 289 ftrace_event.name as name, 290 ftrace_event.cpu as cpu, 291 thread.name as thread, 292 process.name as process, 293 to_ftrace(ftrace_event.id) as args 294 from ftrace_event 295 join thread using (utid) 296 left join process on thread.upid = process.upid 297 where 298 ftrace_event.name not in (${excludeListSql}) and 299 ts >= ${start} and ts <= ${end} 300 order by id 301 limit ${count} offset ${offset};`); 302 const events: FtraceEvent[] = []; 303 const it = queryRes.iter({ 304 id: NUM, 305 ts: LONG, 306 name: STR, 307 cpu: NUM, 308 thread: STR_NULL, 309 process: STR_NULL, 310 args: STR, 311 }); 312 for (let row = 0; it.valid(); it.next(), row++) { 313 events.push({ 314 id: it.id, 315 ts: Time.fromRaw(it.ts), 316 name: it.name, 317 cpu: it.cpu, 318 thread: it.thread, 319 process: it.process, 320 args: it.args, 321 }); 322 } 323 return {events, offset, numEvents}; 324} 325