blob: bc56498561d7ab3dccfc37ae5ec2ff1322591e05 [file] [log] [blame]
// Copyright (C) 2024 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 {AsyncLimiter} from '../base/async_limiter';
import {searchSegment} from '../base/binary_search';
import {assertExists, assertTrue} from '../base/logging';
import {sqliteString} from '../base/string_utils';
import {Time, TimeSpan} from '../base/time';
import {exists} from '../base/utils';
import {ResultStepEventHandler} from '../public/search';
import {
ANDROID_LOGS_TRACK_KIND,
CPU_SLICE_TRACK_KIND,
} from '../public/track_kinds';
import {Workspace} from '../public/workspace';
import {Engine} from '../trace_processor/engine';
import {LONG, NUM, STR} from '../trace_processor/query_result';
import {escapeSearchQuery} from '../trace_processor/query_utils';
import {raf} from './raf_scheduler';
import {SearchSource} from './search_data';
import {TimelineImpl} from './timeline';
import {TrackManagerImpl} from './track_manager';
export interface SearchResults {
eventIds: Float64Array;
tses: BigInt64Array;
utids: Float64Array;
trackUris: string[];
sources: SearchSource[];
totalResults: number;
}
export class SearchManagerImpl {
private _searchGeneration = 0;
private _searchText = '';
private _searchWindow?: TimeSpan;
private _results?: SearchResults;
private _resultIndex = -1;
private _searchInProgress = false;
// TODO(primiano): once we get rid of globals, these below can be made always
// defined. the ?: is to deal with globals-before-trace-load.
private _timeline?: TimelineImpl;
private _trackManager?: TrackManagerImpl;
private _workspace?: Workspace;
private _engine?: Engine;
private _limiter = new AsyncLimiter();
private _onResultStep?: ResultStepEventHandler;
constructor(args?: {
timeline: TimelineImpl;
trackManager: TrackManagerImpl;
workspace: Workspace;
engine: Engine;
onResultStep: ResultStepEventHandler;
}) {
this._timeline = args?.timeline;
this._trackManager = args?.trackManager;
this._engine = args?.engine;
this._workspace = args?.workspace;
this._onResultStep = args?.onResultStep;
}
search(text: string) {
if (text === this._searchText) {
return;
}
this._searchText = text;
this._searchGeneration++;
this._results = undefined;
this._resultIndex = -1;
this._searchInProgress = false;
if (text !== '') {
this._searchInProgress = true;
this._searchWindow = this._timeline?.visibleWindow.toTimeSpan();
this._limiter.schedule(async () => {
await this.executeSearch();
this._searchInProgress = false;
raf.scheduleFullRedraw();
});
}
raf.scheduleFullRedraw();
}
reset() {
this.search('');
}
stepForward() {
this.stepInternal(false);
}
stepBackwards() {
this.stepInternal(true);
}
private stepInternal(reverse = false) {
if (this._searchWindow === undefined) return;
if (this._results === undefined) return;
const index = this._resultIndex;
const {start: startNs, end: endNs} = this._searchWindow;
const currentTs = this._results.tses[index];
// If the value of |this._results.totalResults| is 0,
// it means that the query is in progress or no results are found.
if (this._results.totalResults === 0) {
return;
}
// If this is a new search or the currentTs is not in the viewport,
// select the first/last item in the viewport.
if (
index === -1 ||
(currentTs !== -1n && (currentTs < startNs || currentTs > endNs))
) {
if (reverse) {
const [smaller] = searchSegment(this._results.tses, endNs);
// If there is no item in the viewport just go to the previous.
if (smaller === -1) {
this.setResultIndexWithSaturation(index - 1);
} else {
this._resultIndex = smaller;
}
} else {
const [, larger] = searchSegment(this._results.tses, startNs);
// If there is no item in the viewport just go to the next.
if (larger === -1) {
this.setResultIndexWithSaturation(index + 1);
} else {
this._resultIndex = larger;
}
}
} else {
// If the currentTs is in the viewport, increment the index.
if (reverse) {
this.setResultIndexWithSaturation(index - 1);
} else {
this.setResultIndexWithSaturation(index + 1);
}
}
if (this._onResultStep) {
this._onResultStep({
eventId: this._results.eventIds[this._resultIndex],
ts: Time.fromRaw(this._results.tses[this._resultIndex]),
trackUri: this._results.trackUris[this._resultIndex],
source: this._results.sources[this._resultIndex],
});
}
raf.scheduleFullRedraw();
}
private setResultIndexWithSaturation(nextIndex: number) {
const tot = assertExists(this._results).totalResults;
assertTrue(tot !== 0); // The early out in step() guarantees this.
// This is a modulo operation that works also for negative numbers.
this._resultIndex = ((nextIndex % tot) + tot) % tot;
}
get hasResults() {
return this._results !== undefined;
}
get searchResults() {
return this._results;
}
get resultIndex() {
return this._resultIndex;
}
get searchText() {
return this._searchText;
}
get searchGeneration() {
return this._searchGeneration;
}
get searchInProgress(): boolean {
return this._searchInProgress;
}
private async executeSearch() {
const search = this._searchText;
const window = this._searchWindow;
const searchLiteral = escapeSearchQuery(this._searchText);
const generation = this._searchGeneration;
const engine = this._engine;
const trackManager = this._trackManager;
const workspace = this._workspace;
if (!engine || !trackManager || !workspace || !window) {
return;
}
// TODO(stevegolton): Avoid recomputing these indexes each time.
const trackUrisByCpu = new Map<number, string>();
const allTracks = trackManager.getAllTracks();
allTracks.forEach((td) => {
const tags = td?.tags;
const cpu = tags?.cpu;
const kind = tags?.kind;
exists(cpu) &&
kind === CPU_SLICE_TRACK_KIND &&
trackUrisByCpu.set(cpu, td.uri);
});
const trackUrisByTrackId = new Map<number, string>();
allTracks.forEach((td) => {
const trackIds = td?.tags?.trackIds ?? [];
trackIds.forEach((trackId) => trackUrisByTrackId.set(trackId, td.uri));
});
const utidRes = await engine.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 engine.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: SearchResults = {
eventIds: new Float64Array(0),
tses: new BigInt64Array(0),
utids: new Float64Array(0),
sources: [],
trackUris: [],
totalResults: 0,
};
const lowerSearch = search.toLowerCase();
for (const track of workspace.flatTracks) {
// We don't support searching for tracks that don't have a URI.
if (!track.uri) continue;
if (track.title.toLowerCase().indexOf(lowerSearch) === -1) {
continue;
}
searchResults.totalResults++;
searchResults.sources.push('track');
searchResults.trackUris.push(track.uri);
}
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 track: string | undefined = undefined;
if (it.source === 'cpu') {
track = trackUrisByCpu.get(it.sourceId);
} else if (it.source === 'slice') {
track = trackUrisByTrackId.get(it.sourceId);
} else if (it.source === 'log') {
track = trackManager
.getAllTracks()
.find((td) => td.tags?.kind === ANDROID_LOGS_TRACK_KIND)?.uri;
}
// The .get() calls above could return undefined, this isn't just an else.
if (track === undefined) {
continue;
}
const i = searchResults.totalResults++;
searchResults.trackUris.push(track);
searchResults.sources.push(it.source as SearchSource);
searchResults.eventIds[i] = it.sliceId;
searchResults.tses[i] = it.ts;
searchResults.utids[i] = it.utid;
}
if (generation !== this._searchGeneration) {
// We arrived too late. By the time we computed results the user issued
// another search.
return;
}
this._results = searchResults;
this._resultIndex = -1;
}
}