blob: b7d2c4512f1ff94998ea9acc296eb3c32da36b41 [file] [log] [blame]
// Copyright (C) 2019 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 {sqliteString} from '../base/string_utils';
import {exists} from '../base/utils';
import {CurrentSearchResults, SearchSource} from '../common/search_data';
import {OmniboxState} from '../common/state';
import {CPU_SLICE_TRACK_KIND} from '../core/track_kinds';
import {globals} from '../frontend/globals';
import {publishSearchResult} from '../frontend/publish';
import {Engine} from '../trace_processor/engine';
import {LONG, NUM, STR} from '../trace_processor/query_result';
import {escapeSearchQuery} from '../trace_processor/query_utils';
import {Controller} from './controller';
export interface SearchControllerArgs {
engine: Engine;
}
export class SearchController extends Controller<'main'> {
private engine: Engine;
private previousOmniboxState?: OmniboxState;
private updateInProgress: boolean;
constructor(args: SearchControllerArgs) {
super('main');
this.engine = args.engine;
this.updateInProgress = false;
}
run() {
if (this.updateInProgress) {
return;
}
const omniboxState = globals.state.omniboxState;
if (omniboxState === undefined || omniboxState.mode === 'COMMAND') {
return;
}
const newOmniboxState = omniboxState;
if (this.previousOmniboxState === newOmniboxState) {
return;
}
// TODO(hjd): We should restrict this to the start of the trace but
// that is not easily available here.
// N.B. Timestamps can be negative.
this.previousOmniboxState = newOmniboxState;
const search = newOmniboxState.omnibox;
if (search === '' || (search.length < 4 && !newOmniboxState.force)) {
publishSearchResult({
eventIds: new Float64Array(0),
tses: new BigInt64Array(0),
utids: new Float64Array(0),
sources: [],
trackKeys: [],
totalResults: 0,
});
return;
}
const computeResults = this.specificSearch(search).then((searchResults) => {
publishSearchResult(searchResults);
});
Promise.all([computeResults]).finally(() => {
this.updateInProgress = false;
this.run();
});
}
onDestroy() {}
private async specificSearch(search: string) {
const searchLiteral = escapeSearchQuery(search);
// TODO(hjd): we should avoid recomputing this every time. This will be
// easier once the track table has entries for all the tracks.
const cpuToTrackId = new Map();
for (const track of Object.values(globals.state.tracks)) {
const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
exists(trackInfo.tags.cpu) &&
cpuToTrackId.set(trackInfo.tags.cpu, track.key);
}
}
const utidRes = await this.query(`select utid from thread join process
using(upid) where
thread.name glob ${searchLiteral} or
process.name glob ${searchLiteral}`);
const utids = [];
for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
utids.push(it.utid);
}
const res = await this.query(`
select
id as sliceId,
ts,
'cpu' as source,
cpu as sourceId,
utid
from sched where utid in (${utids.join(',')})
union all
select *
from (
select
slice_id as sliceId,
ts,
'slice' as source,
track_id as sourceId,
0 as utid
from slice
where slice.name glob ${searchLiteral}
or (
0 != CAST(${sqliteString(search)} AS INT) and
sliceId = CAST(${sqliteString(search)} AS INT)
)
union
select
slice_id as sliceId,
ts,
'slice' as source,
track_id as sourceId,
0 as utid
from slice
join args using(arg_set_id)
where string_value glob ${searchLiteral} or key glob ${searchLiteral}
)
union all
select
id as sliceId,
ts,
'log' as source,
0 as sourceId,
utid
from android_logs where msg glob ${searchLiteral}
order by ts
`);
const searchResults: CurrentSearchResults = {
eventIds: new Float64Array(0),
tses: new BigInt64Array(0),
utids: new Float64Array(0),
sources: [],
trackKeys: [],
totalResults: 0,
};
const lowerSearch = search.toLowerCase();
for (const track of Object.values(globals.state.tracks)) {
if (track.name.toLowerCase().indexOf(lowerSearch) === -1) {
continue;
}
searchResults.totalResults++;
searchResults.sources.push('track');
searchResults.trackKeys.push(track.key);
}
const rows = res.numRows();
searchResults.eventIds = new Float64Array(
searchResults.totalResults + rows,
);
searchResults.tses = new BigInt64Array(searchResults.totalResults + rows);
searchResults.utids = new Float64Array(searchResults.totalResults + rows);
for (let i = 0; i < searchResults.totalResults; ++i) {
searchResults.eventIds[i] = -1;
searchResults.tses[i] = -1n;
searchResults.utids[i] = -1;
}
const it = res.iter({
sliceId: NUM,
ts: LONG,
source: STR,
sourceId: NUM,
utid: NUM,
});
for (; it.valid(); it.next()) {
let trackId = undefined;
if (it.source === 'cpu') {
trackId = cpuToTrackId.get(it.sourceId);
} else if (it.source === 'slice') {
trackId = globals.trackManager.trackKeyByTrackId.get(it.sourceId);
} else if (it.source === 'log') {
const logTracks = Object.values(globals.state.tracks).filter(
(track) => {
const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
return trackDesc && trackDesc.tags?.kind === 'AndroidLogTrack';
},
);
if (logTracks.length > 0) {
trackId = logTracks[0].key;
}
}
// The .get() calls above could return undefined, this isn't just an else.
if (trackId === undefined) {
continue;
}
const i = searchResults.totalResults++;
searchResults.trackKeys.push(trackId);
searchResults.sources.push(it.source as SearchSource);
searchResults.eventIds[i] = it.sliceId;
searchResults.tses[i] = it.ts;
searchResults.utids[i] = it.utid;
}
return searchResults;
}
private async query(query: string) {
const result = await this.engine.query(query);
return result;
}
}