|  | // Copyright (C) 2023 The Android Open Source Project | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this 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 {Span, Time} from '../base/time'; | 
|  | import { | 
|  | HighPrecisionTime, | 
|  | HighPrecisionTimeSpan, | 
|  | } from '../common/high_precision_time'; | 
|  | import {FtraceFilterState, Pagination} from '../common/state'; | 
|  | import {FtraceEvent, globals} from '../frontend/globals'; | 
|  | import {publishFtracePanelData} from '../frontend/publish'; | 
|  | import {ratelimit} from '../frontend/rate_limiters'; | 
|  | import {Engine} from '../trace_processor/engine'; | 
|  | import {LONG, NUM, STR, STR_NULL} from '../trace_processor/query_result'; | 
|  |  | 
|  | import {Controller} from './controller'; | 
|  |  | 
|  | export interface FtraceControllerArgs { | 
|  | engine: Engine; | 
|  | } | 
|  |  | 
|  | interface RetVal { | 
|  | events: FtraceEvent[]; | 
|  | offset: number; | 
|  | numEvents: number; | 
|  | } | 
|  |  | 
|  | export class FtraceController extends Controller<'main'> { | 
|  | private engine: Engine; | 
|  | private oldSpan: Span<HighPrecisionTime> = HighPrecisionTimeSpan.ZERO; | 
|  | private oldFtraceFilter?: FtraceFilterState; | 
|  | private oldPagination?: Pagination; | 
|  |  | 
|  | constructor({engine}: FtraceControllerArgs) { | 
|  | super('main'); | 
|  | this.engine = engine; | 
|  | } | 
|  |  | 
|  | run() { | 
|  | if (this.shouldUpdate()) { | 
|  | this.oldSpan = globals.timeline.visibleWindowTime; | 
|  | this.oldFtraceFilter = globals.state.ftraceFilter; | 
|  | this.oldPagination = globals.state.ftracePagination; | 
|  | if (globals.state.ftracePagination.count > 0) { | 
|  | this.lookupFtraceEventsRateLimited(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | private lookupFtraceEventsRateLimited = ratelimit(() => { | 
|  | const {offset, count} = globals.state.ftracePagination; | 
|  | // The formatter doesn't like formatted chained methods :( | 
|  | const promise = this.lookupFtraceEvents(offset, count); | 
|  | promise.then(({events, offset, numEvents}: RetVal) => { | 
|  | publishFtracePanelData({events, offset, numEvents}); | 
|  | }); | 
|  | }, 250); | 
|  |  | 
|  | private shouldUpdate(): boolean { | 
|  | // Has the visible window moved? | 
|  | const visibleWindow = globals.timeline.visibleWindowTime; | 
|  | if (!this.oldSpan.equals(visibleWindow)) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // Has the pagination changed? | 
|  | if (this.oldPagination !== globals.state.ftracePagination) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // Has the filter changed? | 
|  | if (this.oldFtraceFilter !== globals.state.ftraceFilter) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | return false; | 
|  | } | 
|  |  | 
|  | async lookupFtraceEvents(offset: number, count: number): Promise<RetVal> { | 
|  | const appState = globals.state; | 
|  | const {start, end} = globals.stateVisibleTime(); | 
|  |  | 
|  | const excludeList = appState.ftraceFilter.excludedNames; | 
|  | const excludeListSql = excludeList.map((s) => `'${s}'`).join(','); | 
|  |  | 
|  | // TODO(stevegolton): This query can be slow when traces are huge. | 
|  | // The number of events is only used for correctly sizing the panel's | 
|  | // scroll container so that the scrollbar works as if the panel were fully | 
|  | // populated. | 
|  | // Perhaps we could work out some UX that doesn't need this. | 
|  | let queryRes = await this.engine.query(` | 
|  | select count(id) as numEvents | 
|  | from ftrace_event | 
|  | where | 
|  | ftrace_event.name not in (${excludeListSql}) and | 
|  | ts >= ${start} and ts <= ${end} | 
|  | `); | 
|  | const {numEvents} = queryRes.firstRow({numEvents: NUM}); | 
|  |  | 
|  | queryRes = await this.engine.query(` | 
|  | select | 
|  | ftrace_event.id as id, | 
|  | ftrace_event.ts as ts, | 
|  | ftrace_event.name as name, | 
|  | ftrace_event.cpu as cpu, | 
|  | thread.name as thread, | 
|  | process.name as process, | 
|  | to_ftrace(ftrace_event.id) as args | 
|  | from ftrace_event | 
|  | left join thread | 
|  | on ftrace_event.utid = thread.utid | 
|  | left join process | 
|  | on thread.upid = process.upid | 
|  | where | 
|  | ftrace_event.name not in (${excludeListSql}) and | 
|  | ts >= ${start} and ts <= ${end} | 
|  | order by id | 
|  | limit ${count} offset ${offset};`); | 
|  | const events: FtraceEvent[] = []; | 
|  | const it = queryRes.iter({ | 
|  | id: NUM, | 
|  | ts: LONG, | 
|  | name: STR, | 
|  | cpu: NUM, | 
|  | thread: STR_NULL, | 
|  | process: STR_NULL, | 
|  | args: STR, | 
|  | }); | 
|  | for (let row = 0; it.valid(); it.next(), row++) { | 
|  | events.push({ | 
|  | id: it.id, | 
|  | ts: Time.fromRaw(it.ts), | 
|  | name: it.name, | 
|  | cpu: it.cpu, | 
|  | thread: it.thread, | 
|  | process: it.process, | 
|  | args: it.args, | 
|  | }); | 
|  | } | 
|  | return {events, offset, numEvents}; | 
|  | } | 
|  | } |