blob: f558a4a76f7580e43eb317b5989a6335f1e17c5c [file] [log] [blame]
// 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 {NUM, STR, STR_NULL} from '../common/query_result';
import {FtraceFilterState, Pagination} from '../common/state';
import {TimeSpan, toNsCeil, toNsFloor} 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: TimeSpan = new TimeSpan(0, 0);
private oldFtraceFilter?: FtraceFilterState;
private oldPagination?: Pagination;
constructor({engine}: FtraceControllerArgs) {
super('main');
this.engine = engine;
}
run() {
if (this.shouldUpdate()) {
this.oldSpan = globals.frontendLocalState.visibleWindowTime.clone();
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.start !== visibleWindow.start ||
this.oldSpan.end !== visibleWindow.end) {
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 frontendState = globals.frontendLocalState;
const {start, end} = frontendState.visibleWindowTime;
const startNs = toNsFloor(start);
const endNs = toNsCeil(end);
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: NUM,
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};
}
};