// 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 {Engine} from '../common/engine'; import { HighPrecisionTime, HighPrecisionTimeSpan, } from '../common/high_precision_time'; import {LONG, NUM, STR, STR_NULL} from '../common/query_result'; import {FtraceFilterState, Pagination} from '../common/state'; import {Span} from '../common/time'; import {FtraceEvent, globals} from '../frontend/globals'; import {publishFtracePanelData} from '../frontend/publish'; import {ratelimit} from '../frontend/rate_limiters'; 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 = HighPrecisionTimeSpan.ZERO; private oldFtraceFilter?: FtraceFilterState; private oldPagination?: Pagination; constructor({engine}: FtraceControllerArgs) { super('main'); this.engine = engine; } run() { if (this.shouldUpdate()) { this.oldSpan = globals.frontendLocalState.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.frontendLocalState.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 { const appState = globals.state; const frontendState = globals.frontendLocalState; const {start, end} = frontendState.visibleWindowTime; const startNs = start.nanos; const endNs = end.nanos; 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 >= ${startNs} and ts <= ${endNs} `); 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 >= ${startNs} and ts <= ${endNs} 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: it.ts, name: it.name, cpu: it.cpu, thread: it.thread, process: it.process, args: it.args, }); } return {events, offset, numEvents}; } };