| // 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 {Engine} from '../common/engine'; |
| import {CurrentSearchResults, SearchSummary} from '../common/search_data'; |
| import {TimeSpan} from '../common/time'; |
| |
| import {Controller} from './controller'; |
| import {App} from './globals'; |
| |
| 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 ref_type ref, 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; |
| const 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, 0), |
| 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), |
| refTypes: [], |
| trackIds: [], |
| totalResults: 0, |
| }); |
| return; |
| } |
| |
| const startNs = Math.round(newSpan.start * 1e9); |
| const endNs = Math.round(newSpan.end * 1e9); |
| 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]) |
| .catch(e => { |
| console.error(e); |
| }) |
| .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); |
| |
| 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 "%${search}%" or process.name like "%${ |
| search}%"`); |
| |
| const utids = [...rawUtidResult.columns[0].longValues!]; |
| |
| const maxCpu = Math.max(...await this.engine.getCpus()); |
| |
| 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 '%${search}%' |
| ) |
| group by quantum_ts |
| order by quantum_ts;`); |
| |
| const numRows = +rawResult.numRecords; |
| 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) { |
| // 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' || |
| track.kind === 'AsyncSliceTrack') { |
| engineTrackIdToTrackId.set( |
| (track.config as {trackId: number}).trackId, track.id); |
| continue; |
| } |
| } |
| |
| const rawUtidResult = await this.query(`select utid from thread join process |
| using(upid) where thread.name like "%${search}%" or process.name like "%${ |
| search}%"`); |
| const utids = [...rawUtidResult.columns[0].longValues!]; |
| |
| const rawResult = await this.query(` |
| select |
| row_id as slice_id, |
| ts, |
| 'cpu' as source, |
| cpu as ref, |
| utid |
| from sched where utid in (${utids.join(',')}) |
| union |
| select |
| slice_id, |
| ts, |
| 'track' as source, |
| track_id as ref, |
| 0 as utid |
| from slice |
| inner join track on slice.track_id = track.id |
| and slice.name like '%${search}%' |
| order by ts`); |
| |
| const numRows = +rawResult.numRecords; |
| |
| const searchResults: CurrentSearchResults = { |
| sliceIds: new Float64Array(numRows), |
| tsStarts: new Float64Array(numRows), |
| utids: new Float64Array(numRows), |
| trackIds: [], |
| refTypes: [], |
| totalResults: +numRows, |
| }; |
| |
| const columns = rawResult.columns; |
| for (let row = 0; row < numRows; row++) { |
| const source = columns[2].stringValues![row]; |
| const ref = +columns[3].longValues![row]; |
| let trackId = undefined; |
| if (source === 'cpu') { |
| trackId = cpuToTrackId.get(ref); |
| } else if (source === 'track') { |
| trackId = engineTrackIdToTrackId.get(ref); |
| } |
| |
| if (trackId === undefined) { |
| searchResults.totalResults--; |
| continue; |
| } |
| |
| searchResults.trackIds.push(trackId); |
| searchResults.refTypes.push(source); |
| searchResults.sliceIds[row] = +columns[0].longValues![row]; |
| searchResults.tsStarts[row] = +columns[1].longValues![row]; |
| searchResults.utids[row] = +columns[4].longValues![row]; |
| } |
| return searchResults; |
| } |
| |
| |
| private async query(query: string) { |
| const result = await this.engine.query(query); |
| if (result.error) { |
| console.error(`Query error "${query}": ${result.error}`); |
| throw new Error(`Query error "${query}": ${result.error}`); |
| } |
| return result; |
| } |
| } |