// 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 {TRACE_MARGIN_TIME_S} from '../common/constants'; import {Engine} from '../common/engine'; import {slowlyCountRows} from '../common/query_iterator'; import {CurrentSearchResults, SearchSummary} from '../common/search_data'; import {TimeSpan} from '../common/time'; import {Controller} from './controller'; import {App} from './globals'; export function escapeQuery(s: string): string { // See https://www.sqlite.org/lang_expr.html#:~:text=A%20string%20constant s = s.replace(/\'/g, '\'\''); s = s.replace(/_/g, '^_'); s = s.replace(/%/g, '^%'); return `'%${s}%' escape '^'`; } export interface SearchControllerArgs { engine: Engine; app: App; } export class SearchController extends Controller<'main'> { private engine: Engine; private app: App; private previousSpan: TimeSpan; private previousResolution: number; private previousSearch: string; private updateInProgress: boolean; private setupInProgress: boolean; constructor(args: SearchControllerArgs) { super('main'); this.engine = args.engine; this.app = args.app; this.previousSpan = new TimeSpan(0, 1); this.previousSearch = ''; this.updateInProgress = false; this.setupInProgress = true; this.previousResolution = 1; this.setup().finally(() => { this.setupInProgress = false; this.run(); }); } private async setup() { await this.query(`create virtual table search_summary_window using window;`); await this.query(`create virtual table search_summary_sched_span using span_join(sched PARTITIONED cpu, search_summary_window);`); await this.query(`create virtual table search_summary_slice_span using span_join(slice PARTITIONED track_id, search_summary_window);`); } run() { if (this.setupInProgress || this.updateInProgress) { return; } const visibleState = this.app.state.frontendLocalState.visibleState; const omniboxState = this.app.state.frontendLocalState.omniboxState; if (visibleState === undefined || omniboxState === undefined || omniboxState.mode === 'COMMAND') { return; } const newSpan = new TimeSpan(visibleState.startSec, visibleState.endSec); const newSearch = omniboxState.omnibox; let newResolution = visibleState.resolution; if (this.previousSpan.contains(newSpan) && this.previousResolution === newResolution && newSearch === this.previousSearch) { return; } this.previousSpan = new TimeSpan( Math.max(newSpan.start - newSpan.duration, -TRACE_MARGIN_TIME_S), newSpan.end + newSpan.duration); this.previousResolution = newResolution; this.previousSearch = newSearch; if (newSearch === '' || newSearch.length < 4) { this.app.publish('Search', { tsStarts: new Float64Array(0), tsEnds: new Float64Array(0), count: new Uint8Array(0), }); this.app.publish('SearchResult', { sliceIds: new Float64Array(0), tsStarts: new Float64Array(0), utids: new Float64Array(0), sources: [], trackIds: [], totalResults: 0, }); return; } let startNs = Math.round(newSpan.start * 1e9); let endNs = Math.round(newSpan.end * 1e9); // TODO(hjd): We shouldn't need to be so defensive here: if (!Number.isFinite(startNs)) { startNs = 0; } if (!Number.isFinite(endNs)) { endNs = 1; } if (!Number.isFinite(newResolution)) { newResolution = 1; } this.updateInProgress = true; const computeSummary = this.update(newSearch, startNs, endNs, newResolution).then(summary => { this.app.publish('Search', summary); }); const computeResults = this.specificSearch(newSearch).then(searchResults => { this.app.publish('SearchResult', searchResults); }); Promise.all([computeSummary, computeResults]) .finally(() => { this.updateInProgress = false; this.run(); }); } onDestroy() {} private async update( search: string, startNs: number, endNs: number, resolution: number): Promise<SearchSummary> { const quantumNs = Math.round(resolution * 10 * 1e9); const searchLiteral = escapeQuery(search); startNs = Math.floor(startNs / quantumNs) * quantumNs; await this.query(`update search_summary_window set window_start=${startNs}, window_dur=${endNs - startNs}, quantum=${quantumNs} where rowid = 0;`); const rawUtidResult = await this.query(`select utid from thread join process using(upid) where thread.name like ${searchLiteral} or process.name like ${searchLiteral}`); const utids = [...rawUtidResult.columns[0].longValues!]; const cpus = await this.engine.getCpus(); const maxCpu = Math.max(...cpus, -1); const rawResult = await this.query(` select (quantum_ts * ${quantumNs} + ${startNs})/1e9 as tsStart, ((quantum_ts+1) * ${quantumNs} + ${startNs})/1e9 as tsEnd, min(count(*), 255) as count from ( select quantum_ts from search_summary_sched_span where utid in (${utids.join(',')}) and cpu <= ${maxCpu} union all select quantum_ts from search_summary_slice_span where name like ${searchLiteral} ) group by quantum_ts order by quantum_ts;`); const numRows = slowlyCountRows(rawResult); const summary = { tsStarts: new Float64Array(numRows), tsEnds: new Float64Array(numRows), count: new Uint8Array(numRows) }; const columns = rawResult.columns; for (let row = 0; row < numRows; row++) { summary.tsStarts[row] = +columns[0].doubleValues![row]; summary.tsEnds[row] = +columns[1].doubleValues![row]; summary.count[row] = +columns[2].longValues![row]; } return summary; } private async specificSearch(search: string) { const searchLiteral = escapeQuery(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(); const engineTrackIdToTrackId = new Map(); for (const track of Object.values(this.app.state.tracks)) { if (track.kind === 'CpuSliceTrack') { cpuToTrackId.set((track.config as {cpu: number}).cpu, track.id); continue; } if (track.kind === 'ChromeSliceTrack') { const config = (track.config as {trackId: number}); engineTrackIdToTrackId.set(config.trackId, track.id); continue; } if (track.kind === 'AsyncSliceTrack') { const config = (track.config as {trackIds: number[]}); for (const trackId of config.trackIds) { engineTrackIdToTrackId.set(trackId, track.id); } continue; } } const rawUtidResult = await this.query(`select utid from thread join process using(upid) where thread.name like ${searchLiteral} or process.name like ${searchLiteral}`); const utids = [...rawUtidResult.columns[0].longValues!]; const rawResult = await this.query(` select id as slice_id, ts, 'cpu' as source, cpu as source_id, utid from sched where utid in (${utids.join(',')}) union select slice_id, ts, 'track' as source, track_id as source_id, 0 as utid from slice where slice.name like ${searchLiteral} union select slice_id, ts, 'track' as source, track_id as source_id, 0 as utid from slice join args using(arg_set_id) where string_value like ${searchLiteral} order by ts`); const numRows = slowlyCountRows(rawResult); const searchResults: CurrentSearchResults = { sliceIds: [], tsStarts: [], utids: [], trackIds: [], sources: [], totalResults: +numRows, }; const columns = rawResult.columns; for (let row = 0; row < numRows; row++) { const source = columns[2].stringValues![row]; const sourceId = +columns[3].longValues![row]; let trackId = undefined; if (source === 'cpu') { trackId = cpuToTrackId.get(sourceId); } else if (source === 'track') { trackId = engineTrackIdToTrackId.get(sourceId); } if (trackId === undefined) { searchResults.totalResults--; continue; } searchResults.trackIds.push(trackId); searchResults.sources.push(source); searchResults.sliceIds.push(+columns[0].longValues![row]); searchResults.tsStarts.push(+columns[1].longValues![row]); searchResults.utids.push(+columns[4].longValues![row]); } return searchResults; } private async query(query: string) { const result = await this.engine.query(query); return result; } }