1// Copyright (C) 2019 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {sqliteString} from '../base/string_utils'; 16import {Duration, duration, Span, time, Time, TimeSpan} from '../base/time'; 17import {exists} from '../base/utils'; 18import { 19 CurrentSearchResults, 20 SearchSource, 21 SearchSummary, 22} from '../common/search_data'; 23import {OmniboxState} from '../common/state'; 24import {CPU_SLICE_TRACK_KIND} from '../core/track_kinds'; 25import {globals} from '../frontend/globals'; 26import {publishSearch, publishSearchResult} from '../frontend/publish'; 27import {Engine} from '../trace_processor/engine'; 28import {LONG, NUM, STR} from '../trace_processor/query_result'; 29import {escapeSearchQuery} from '../trace_processor/query_utils'; 30 31import {Controller} from './controller'; 32 33export interface SearchControllerArgs { 34 engine: Engine; 35} 36 37export class SearchController extends Controller<'main'> { 38 private engine: Engine; 39 private previousSpan: Span<time, duration>; 40 private previousResolution: duration; 41 private previousOmniboxState?: OmniboxState; 42 private updateInProgress: boolean; 43 private setupInProgress: boolean; 44 45 constructor(args: SearchControllerArgs) { 46 super('main'); 47 this.engine = args.engine; 48 this.previousSpan = new TimeSpan(Time.fromRaw(0n), Time.fromRaw(1n)); 49 this.updateInProgress = false; 50 this.setupInProgress = true; 51 this.previousResolution = 1n; 52 this.setup().finally(() => { 53 this.setupInProgress = false; 54 this.run(); 55 }); 56 } 57 58 private async setup() { 59 await this.query(`create virtual table search_summary_window 60 using window;`); 61 await this.query(`create virtual table search_summary_sched_span using 62 span_join(sched PARTITIONED cpu, search_summary_window);`); 63 await this.query(`create virtual table search_summary_slice_span using 64 span_join(slice PARTITIONED track_id, search_summary_window);`); 65 } 66 67 run() { 68 if (this.setupInProgress || this.updateInProgress) { 69 return; 70 } 71 72 const visibleState = globals.state.frontendLocalState.visibleState; 73 const omniboxState = globals.state.omniboxState; 74 if ( 75 visibleState === undefined || 76 omniboxState === undefined || 77 omniboxState.mode === 'COMMAND' 78 ) { 79 return; 80 } 81 const newSpan = globals.stateVisibleTime(); 82 const newOmniboxState = omniboxState; 83 const newResolution = visibleState.resolution; 84 if ( 85 this.previousSpan.contains(newSpan) && 86 this.previousResolution === newResolution && 87 this.previousOmniboxState === newOmniboxState 88 ) { 89 return; 90 } 91 92 // TODO(hjd): We should restrict this to the start of the trace but 93 // that is not easily available here. 94 // N.B. Timestamps can be negative. 95 const {start, end} = newSpan.pad(newSpan.duration); 96 this.previousSpan = new TimeSpan(start, end); 97 this.previousResolution = newResolution; 98 this.previousOmniboxState = newOmniboxState; 99 const search = newOmniboxState.omnibox; 100 if (search === '' || (search.length < 4 && !newOmniboxState.force)) { 101 publishSearch({ 102 tsStarts: new BigInt64Array(0), 103 tsEnds: new BigInt64Array(0), 104 count: new Uint8Array(0), 105 }); 106 publishSearchResult({ 107 eventIds: new Float64Array(0), 108 tses: new BigInt64Array(0), 109 utids: new Float64Array(0), 110 sources: [], 111 trackKeys: [], 112 totalResults: 0, 113 }); 114 return; 115 } 116 117 this.updateInProgress = true; 118 const computeSummary = this.update( 119 search, 120 newSpan.start, 121 newSpan.end, 122 newResolution, 123 ).then((summary) => { 124 publishSearch(summary); 125 }); 126 127 const computeResults = this.specificSearch(search).then((searchResults) => { 128 publishSearchResult(searchResults); 129 }); 130 131 Promise.all([computeSummary, computeResults]).finally(() => { 132 this.updateInProgress = false; 133 this.run(); 134 }); 135 } 136 137 onDestroy() {} 138 139 private async update( 140 search: string, 141 start: time, 142 end: time, 143 resolution: duration, 144 ): Promise<SearchSummary> { 145 const searchLiteral = escapeSearchQuery(search); 146 147 const quantum = resolution * 10n; 148 start = Time.quantFloor(start, quantum); 149 150 const windowDur = Duration.max(Time.diff(end, start), 1n); 151 await this.query(`update search_summary_window set 152 window_start=${start}, 153 window_dur=${windowDur}, 154 quantum=${quantum} 155 where rowid = 0;`); 156 157 const utidRes = await this.query(`select utid from thread join process 158 using(upid) where thread.name glob ${searchLiteral} 159 or process.name glob ${searchLiteral}`); 160 161 const utids = []; 162 for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) { 163 utids.push(it.utid); 164 } 165 166 const cpus = globals.traceContext.cpus; 167 const maxCpu = Math.max(...cpus, -1); 168 169 const res = await this.query(` 170 select 171 (quantum_ts * ${quantum} + ${start}) as tsStart, 172 ((quantum_ts+1) * ${quantum} + ${start}) as tsEnd, 173 min(count(*), 255) as count 174 from ( 175 select 176 quantum_ts 177 from search_summary_sched_span 178 where utid in (${utids.join(',')}) and cpu <= ${maxCpu} 179 union all 180 select 181 quantum_ts 182 from search_summary_slice_span 183 where name glob ${searchLiteral} 184 ) 185 group by quantum_ts 186 order by quantum_ts;`); 187 188 const numRows = res.numRows(); 189 const summary: SearchSummary = { 190 tsStarts: new BigInt64Array(numRows), 191 tsEnds: new BigInt64Array(numRows), 192 count: new Uint8Array(numRows), 193 }; 194 195 const it = res.iter({tsStart: LONG, tsEnd: LONG, count: NUM}); 196 for (let row = 0; it.valid(); it.next(), ++row) { 197 summary.tsStarts[row] = it.tsStart; 198 summary.tsEnds[row] = it.tsEnd; 199 summary.count[row] = it.count; 200 } 201 return summary; 202 } 203 204 private async specificSearch(search: string) { 205 const searchLiteral = escapeSearchQuery(search); 206 // TODO(hjd): we should avoid recomputing this every time. This will be 207 // easier once the track table has entries for all the tracks. 208 const cpuToTrackId = new Map(); 209 for (const track of Object.values(globals.state.tracks)) { 210 const trackInfo = globals.trackManager.resolveTrackInfo(track.uri); 211 if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) { 212 exists(trackInfo.cpu) && cpuToTrackId.set(trackInfo.cpu, track.key); 213 } 214 } 215 216 const utidRes = await this.query(`select utid from thread join process 217 using(upid) where 218 thread.name glob ${searchLiteral} or 219 process.name glob ${searchLiteral}`); 220 const utids = []; 221 for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) { 222 utids.push(it.utid); 223 } 224 225 const res = await this.query(` 226 select 227 id as sliceId, 228 ts, 229 'cpu' as source, 230 cpu as sourceId, 231 utid 232 from sched where utid in (${utids.join(',')}) 233 union all 234 select * 235 from ( 236 select 237 slice_id as sliceId, 238 ts, 239 'slice' as source, 240 track_id as sourceId, 241 0 as utid 242 from slice 243 where slice.name glob ${searchLiteral} 244 or ( 245 0 != CAST(${sqliteString(search)} AS INT) and 246 sliceId = CAST(${sqliteString(search)} AS INT) 247 ) 248 union 249 select 250 slice_id as sliceId, 251 ts, 252 'slice' as source, 253 track_id as sourceId, 254 0 as utid 255 from slice 256 join args using(arg_set_id) 257 where string_value glob ${searchLiteral} or key glob ${searchLiteral} 258 ) 259 union all 260 select 261 id as sliceId, 262 ts, 263 'log' as source, 264 0 as sourceId, 265 utid 266 from android_logs where msg glob ${searchLiteral} 267 order by ts 268 `); 269 270 const searchResults: CurrentSearchResults = { 271 eventIds: new Float64Array(0), 272 tses: new BigInt64Array(0), 273 utids: new Float64Array(0), 274 sources: [], 275 trackKeys: [], 276 totalResults: 0, 277 }; 278 279 const lowerSearch = search.toLowerCase(); 280 for (const track of Object.values(globals.state.tracks)) { 281 if (track.name.toLowerCase().indexOf(lowerSearch) === -1) { 282 continue; 283 } 284 searchResults.totalResults++; 285 searchResults.sources.push('track'); 286 searchResults.trackKeys.push(track.key); 287 } 288 289 const rows = res.numRows(); 290 searchResults.eventIds = new Float64Array( 291 searchResults.totalResults + rows, 292 ); 293 searchResults.tses = new BigInt64Array(searchResults.totalResults + rows); 294 searchResults.utids = new Float64Array(searchResults.totalResults + rows); 295 for (let i = 0; i < searchResults.totalResults; ++i) { 296 searchResults.eventIds[i] = -1; 297 searchResults.tses[i] = -1n; 298 searchResults.utids[i] = -1; 299 } 300 301 const it = res.iter({ 302 sliceId: NUM, 303 ts: LONG, 304 source: STR, 305 sourceId: NUM, 306 utid: NUM, 307 }); 308 for (; it.valid(); it.next()) { 309 let trackId = undefined; 310 if (it.source === 'cpu') { 311 trackId = cpuToTrackId.get(it.sourceId); 312 } else if (it.source === 'slice') { 313 trackId = globals.trackManager.trackKeyByTrackId.get(it.sourceId); 314 } else if (it.source === 'log') { 315 const logTracks = Object.values(globals.state.tracks).filter( 316 (track) => { 317 const trackDesc = globals.trackManager.resolveTrackInfo(track.uri); 318 return trackDesc && trackDesc.kind === 'AndroidLogTrack'; 319 }, 320 ); 321 if (logTracks.length > 0) { 322 trackId = logTracks[0].key; 323 } 324 } 325 326 // The .get() calls above could return undefined, this isn't just an else. 327 if (trackId === undefined) { 328 continue; 329 } 330 331 const i = searchResults.totalResults++; 332 searchResults.trackKeys.push(trackId); 333 searchResults.sources.push(it.source as SearchSource); 334 searchResults.eventIds[i] = it.sliceId; 335 searchResults.tses[i] = it.ts; 336 searchResults.utids[i] = it.utid; 337 } 338 return searchResults; 339 } 340 341 private async query(query: string) { 342 const result = await this.engine.query(query); 343 return result; 344 } 345} 346