1// Copyright (C) 2024 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 {AsyncLimiter} from '../base/async_limiter'; 16import {sqliteString} from '../base/string_utils'; 17import {Time} from '../base/time'; 18import {exists} from '../base/utils'; 19import {ResultStepEventHandler} from '../public/search'; 20import { 21 ANDROID_LOGS_TRACK_KIND, 22 CPU_SLICE_TRACK_KIND, 23} from '../public/track_kinds'; 24import {Workspace} from '../public/workspace'; 25import {SourceDataset, UnionDataset} from '../trace_processor/dataset'; 26import {Engine} from '../trace_processor/engine'; 27import {LONG, NUM, STR} from '../trace_processor/query_result'; 28import {escapeSearchQuery} from '../trace_processor/query_utils'; 29import {featureFlags} from './feature_flags'; 30import {raf} from './raf_scheduler'; 31import {SearchSource} from './search_data'; 32import {TimelineImpl} from './timeline'; 33import {TrackManagerImpl} from './track_manager'; 34 35const DATASET_SEARCH = featureFlags.register({ 36 id: 'datasetSearch', 37 name: 'Use dataset search', 38 description: 39 '[Experimental] use dataset for search, which allows searching all tracks with a matching dataset. Might be slower than normal search.', 40 defaultValue: false, 41}); 42 43export interface SearchResults { 44 eventIds: Float64Array; 45 tses: BigInt64Array; 46 utids: Float64Array; 47 trackUris: string[]; 48 sources: SearchSource[]; 49 totalResults: number; 50} 51 52export class SearchManagerImpl { 53 private _searchGeneration = 0; 54 private _searchText = ''; 55 private _results?: SearchResults; 56 private _resultIndex = -1; 57 private _searchInProgress = false; 58 59 // TODO(primiano): once we get rid of globals, these below can be made always 60 // defined. the ?: is to deal with globals-before-trace-load. 61 private _timeline?: TimelineImpl; 62 private _trackManager?: TrackManagerImpl; 63 private _workspace?: Workspace; 64 private _engine?: Engine; 65 private _limiter = new AsyncLimiter(); 66 private _onResultStep?: ResultStepEventHandler; 67 68 constructor(args?: { 69 timeline: TimelineImpl; 70 trackManager: TrackManagerImpl; 71 workspace: Workspace; 72 engine: Engine; 73 onResultStep: ResultStepEventHandler; 74 }) { 75 this._timeline = args?.timeline; 76 this._trackManager = args?.trackManager; 77 this._engine = args?.engine; 78 this._workspace = args?.workspace; 79 this._onResultStep = args?.onResultStep; 80 } 81 82 search(text: string) { 83 if (text === this._searchText) { 84 return; 85 } 86 this._searchText = text; 87 this._searchGeneration++; 88 this._results = undefined; 89 this._resultIndex = -1; 90 this._searchInProgress = false; 91 if (text !== '') { 92 this._searchInProgress = true; 93 this._limiter.schedule(async () => { 94 if (DATASET_SEARCH.get()) { 95 await this.executeDatasetSearch(); 96 } else { 97 await this.executeSearch(); 98 } 99 this._searchInProgress = false; 100 raf.scheduleFullRedraw(); 101 }); 102 } 103 } 104 105 reset() { 106 this.search(''); 107 } 108 109 stepForward() { 110 this.stepInternal(false); 111 } 112 113 stepBackwards() { 114 this.stepInternal(true); 115 } 116 117 private stepInternal(reverse = false) { 118 if (this._results === undefined) return; 119 120 // If the value of |this._results.totalResults| is 0, 121 // it means that the query is in progress or no results are found. 122 if (this._results.totalResults === 0) { 123 return; 124 } 125 126 if (reverse) { 127 --this._resultIndex; 128 if (this._resultIndex < 0) { 129 this._resultIndex = this._results.totalResults - 1; 130 } 131 } else { 132 ++this._resultIndex; 133 if (this._resultIndex > this._results.totalResults - 1) { 134 this._resultIndex = 0; 135 } 136 } 137 this._onResultStep?.({ 138 eventId: this._results.eventIds[this._resultIndex], 139 ts: Time.fromRaw(this._results.tses[this._resultIndex]), 140 trackUri: this._results.trackUris[this._resultIndex], 141 source: this._results.sources[this._resultIndex], 142 }); 143 } 144 145 get hasResults() { 146 return this._results !== undefined; 147 } 148 149 get searchResults() { 150 return this._results; 151 } 152 153 get resultIndex() { 154 return this._resultIndex; 155 } 156 157 get searchText() { 158 return this._searchText; 159 } 160 161 get searchGeneration() { 162 return this._searchGeneration; 163 } 164 165 get searchInProgress(): boolean { 166 return this._searchInProgress; 167 } 168 169 private async executeSearch() { 170 const search = this._searchText; 171 const searchLiteral = escapeSearchQuery(this._searchText); 172 const generation = this._searchGeneration; 173 174 const engine = this._engine; 175 const trackManager = this._trackManager; 176 const workspace = this._workspace; 177 if (!engine || !trackManager || !workspace) { 178 return; 179 } 180 181 // TODO(stevegolton): Avoid recomputing these indexes each time. 182 const trackUrisByCpu = new Map<number, string>(); 183 const allTracks = trackManager.getAllTracks(); 184 allTracks.forEach((td) => { 185 const tags = td?.tags; 186 const cpu = tags?.cpu; 187 const kind = tags?.kind; 188 exists(cpu) && 189 kind === CPU_SLICE_TRACK_KIND && 190 trackUrisByCpu.set(cpu, td.uri); 191 }); 192 193 const trackUrisByTrackId = new Map<number, string>(); 194 allTracks.forEach((td) => { 195 const trackIds = td?.tags?.trackIds ?? []; 196 trackIds.forEach((trackId) => trackUrisByTrackId.set(trackId, td.uri)); 197 }); 198 199 const utidRes = await engine.query(`select utid from thread join process 200 using(upid) where 201 thread.name glob ${searchLiteral} or 202 process.name glob ${searchLiteral}`); 203 const utids = []; 204 for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) { 205 utids.push(it.utid); 206 } 207 208 const res = await engine.query(` 209 select 210 id as sliceId, 211 ts, 212 'cpu' as source, 213 cpu as sourceId, 214 utid 215 from sched where utid in (${utids.join(',')}) 216 union all 217 select * 218 from ( 219 select 220 slice_id as sliceId, 221 ts, 222 'slice' as source, 223 track_id as sourceId, 224 0 as utid 225 from slice 226 where slice.name glob ${searchLiteral} 227 or ( 228 0 != CAST(${sqliteString(search)} AS INT) and 229 sliceId = CAST(${sqliteString(search)} AS INT) 230 ) 231 union 232 select 233 slice_id as sliceId, 234 ts, 235 'slice' as source, 236 track_id as sourceId, 237 0 as utid 238 from slice 239 join args using(arg_set_id) 240 where string_value glob ${searchLiteral} or key glob ${searchLiteral} 241 ) 242 union all 243 select 244 id as sliceId, 245 ts, 246 'log' as source, 247 0 as sourceId, 248 utid 249 from android_logs where msg glob ${searchLiteral} 250 order by ts 251 `); 252 253 const searchResults: SearchResults = { 254 eventIds: new Float64Array(0), 255 tses: new BigInt64Array(0), 256 utids: new Float64Array(0), 257 sources: [], 258 trackUris: [], 259 totalResults: 0, 260 }; 261 262 const lowerSearch = search.toLowerCase(); 263 for (const track of workspace.flatTracksOrdered) { 264 // We don't support searching for tracks that don't have a URI. 265 if (!track.uri) continue; 266 if (track.title.toLowerCase().indexOf(lowerSearch) === -1) { 267 continue; 268 } 269 searchResults.totalResults++; 270 searchResults.sources.push('track'); 271 searchResults.trackUris.push(track.uri); 272 } 273 274 const rows = res.numRows(); 275 searchResults.eventIds = new Float64Array( 276 searchResults.totalResults + rows, 277 ); 278 searchResults.tses = new BigInt64Array(searchResults.totalResults + rows); 279 searchResults.utids = new Float64Array(searchResults.totalResults + rows); 280 for (let i = 0; i < searchResults.totalResults; ++i) { 281 searchResults.eventIds[i] = -1; 282 searchResults.tses[i] = -1n; 283 searchResults.utids[i] = -1; 284 } 285 286 const it = res.iter({ 287 sliceId: NUM, 288 ts: LONG, 289 source: STR, 290 sourceId: NUM, 291 utid: NUM, 292 }); 293 for (; it.valid(); it.next()) { 294 let track: string | undefined = undefined; 295 296 if (it.source === 'cpu') { 297 track = trackUrisByCpu.get(it.sourceId); 298 } else if (it.source === 'slice') { 299 track = trackUrisByTrackId.get(it.sourceId); 300 } else if (it.source === 'log') { 301 track = trackManager 302 .getAllTracks() 303 .find((td) => td.tags?.kind === ANDROID_LOGS_TRACK_KIND)?.uri; 304 } 305 // The .get() calls above could return undefined, this isn't just an else. 306 if (track === undefined) { 307 continue; 308 } 309 310 const i = searchResults.totalResults++; 311 searchResults.trackUris.push(track); 312 searchResults.sources.push(it.source as SearchSource); 313 searchResults.eventIds[i] = it.sliceId; 314 searchResults.tses[i] = it.ts; 315 searchResults.utids[i] = it.utid; 316 } 317 318 if (generation !== this._searchGeneration) { 319 // We arrived too late. By the time we computed results the user issued 320 // another search. 321 return; 322 } 323 this._results = searchResults; 324 325 // We have changed the search results - try and find the first result that's 326 // after the start of this visible window. 327 const visibleWindow = this._timeline?.visibleWindow.toTimeSpan(); 328 if (visibleWindow) { 329 const foundIndex = this._results.tses.findIndex( 330 (ts) => ts >= visibleWindow.start, 331 ); 332 if (foundIndex === -1) { 333 this._resultIndex = -1; 334 } else { 335 // Store the value before the found one, so that when the user presses 336 // enter we navigate to the correct one. 337 this._resultIndex = foundIndex - 1; 338 } 339 } else { 340 this._resultIndex = -1; 341 } 342 } 343 344 private async executeDatasetSearch() { 345 const trackManager = this._trackManager; 346 const engine = this._engine; 347 if (!engine || !trackManager) { 348 return; 349 } 350 351 const generation = this._searchGeneration; 352 const searchLiteral = escapeSearchQuery(this._searchText); 353 354 const datasets = trackManager 355 .getAllTracks() 356 .map((t) => { 357 const dataset = t.track.getDataset?.(); 358 if (dataset) { 359 return [dataset, t.uri] as const; 360 } else { 361 return undefined; 362 } 363 }) 364 .filter(exists) 365 .filter(([dataset]) => dataset.implements({id: NUM, ts: LONG, name: STR})) 366 .map( 367 ([dataset, uri]) => 368 new SourceDataset({ 369 src: ` 370 select 371 id, 372 ts, 373 name, 374 '${uri}' as uri 375 from (${dataset.query()})`, 376 schema: {id: NUM, ts: LONG, name: STR, uri: STR}, 377 }), 378 ); 379 380 const union = new UnionDataset(datasets); 381 const result = await engine.query(` 382 select 383 id, 384 uri, 385 ts 386 from (${union.query()}) 387 where name glob ${searchLiteral} 388 `); 389 390 const numRows = result.numRows(); 391 const searchResults: SearchResults = { 392 eventIds: new Float64Array(numRows), 393 tses: new BigInt64Array(numRows), 394 utids: new Float64Array(numRows), 395 sources: [], 396 trackUris: [], 397 totalResults: numRows, 398 }; 399 400 let i = 0; 401 for ( 402 const iter = result.iter({id: NUM, ts: LONG, uri: STR}); 403 iter.valid(); 404 iter.next() 405 ) { 406 searchResults.eventIds[i] = iter.id; 407 searchResults.tses[i] = iter.ts; 408 searchResults.utids[i] = -1; // We don't know anything about utids. 409 searchResults.sources.push('event'); 410 searchResults.trackUris.push(iter.uri); 411 ++i; 412 } 413 414 if (generation !== this._searchGeneration) { 415 // We arrived too late. By the time we computed results the user issued 416 // another search. 417 return; 418 } 419 this._results = searchResults; 420 421 // We have changed the search results - try and find the first result that's 422 // after the start of this visible window. 423 const visibleWindow = this._timeline?.visibleWindow.toTimeSpan(); 424 if (visibleWindow) { 425 const foundIndex = this._results.tses.findIndex( 426 (ts) => ts >= visibleWindow.start, 427 ); 428 if (foundIndex === -1) { 429 this._resultIndex = -1; 430 } else { 431 // Store the value before the found one, so that when the user presses 432 // enter we navigate to the correct one. 433 this._resultIndex = foundIndex - 1; 434 } 435 } else { 436 this._resultIndex = -1; 437 } 438 } 439} 440