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 {searchSegment} from '../base/binary_search'; 17import {assertExists, assertTrue} from '../base/logging'; 18import {sqliteString} from '../base/string_utils'; 19import {Time, TimeSpan} from '../base/time'; 20import {exists} from '../base/utils'; 21import {ResultStepEventHandler} from '../public/search'; 22import { 23 ANDROID_LOGS_TRACK_KIND, 24 CPU_SLICE_TRACK_KIND, 25} from '../public/track_kinds'; 26import {Workspace} from '../public/workspace'; 27import {Engine} from '../trace_processor/engine'; 28import {LONG, NUM, STR} from '../trace_processor/query_result'; 29import {escapeSearchQuery} from '../trace_processor/query_utils'; 30import {raf} from './raf_scheduler'; 31import {SearchSource} from './search_data'; 32import {TimelineImpl} from './timeline'; 33import {TrackManagerImpl} from './track_manager'; 34 35export interface SearchResults { 36 eventIds: Float64Array; 37 tses: BigInt64Array; 38 utids: Float64Array; 39 trackUris: string[]; 40 sources: SearchSource[]; 41 totalResults: number; 42} 43 44export class SearchManagerImpl { 45 private _searchGeneration = 0; 46 private _searchText = ''; 47 private _searchWindow?: TimeSpan; 48 private _results?: SearchResults; 49 private _resultIndex = -1; 50 private _searchInProgress = false; 51 52 // TODO(primiano): once we get rid of globals, these below can be made always 53 // defined. the ?: is to deal with globals-before-trace-load. 54 private _timeline?: TimelineImpl; 55 private _trackManager?: TrackManagerImpl; 56 private _workspace?: Workspace; 57 private _engine?: Engine; 58 private _limiter = new AsyncLimiter(); 59 private _onResultStep?: ResultStepEventHandler; 60 61 constructor(args?: { 62 timeline: TimelineImpl; 63 trackManager: TrackManagerImpl; 64 workspace: Workspace; 65 engine: Engine; 66 onResultStep: ResultStepEventHandler; 67 }) { 68 this._timeline = args?.timeline; 69 this._trackManager = args?.trackManager; 70 this._engine = args?.engine; 71 this._workspace = args?.workspace; 72 this._onResultStep = args?.onResultStep; 73 } 74 75 search(text: string) { 76 if (text === this._searchText) { 77 return; 78 } 79 this._searchText = text; 80 this._searchGeneration++; 81 this._results = undefined; 82 this._resultIndex = -1; 83 this._searchInProgress = false; 84 if (text !== '') { 85 this._searchInProgress = true; 86 this._searchWindow = this._timeline?.visibleWindow.toTimeSpan(); 87 this._limiter.schedule(async () => { 88 await this.executeSearch(); 89 this._searchInProgress = false; 90 raf.scheduleFullRedraw(); 91 }); 92 } 93 raf.scheduleFullRedraw(); 94 } 95 96 reset() { 97 this.search(''); 98 } 99 100 stepForward() { 101 this.stepInternal(false); 102 } 103 104 stepBackwards() { 105 this.stepInternal(true); 106 } 107 108 private stepInternal(reverse = false) { 109 if (this._searchWindow === undefined) return; 110 if (this._results === undefined) return; 111 112 const index = this._resultIndex; 113 const {start: startNs, end: endNs} = this._searchWindow; 114 const currentTs = this._results.tses[index]; 115 116 // If the value of |this._results.totalResults| is 0, 117 // it means that the query is in progress or no results are found. 118 if (this._results.totalResults === 0) { 119 return; 120 } 121 122 // If this is a new search or the currentTs is not in the viewport, 123 // select the first/last item in the viewport. 124 if ( 125 index === -1 || 126 (currentTs !== -1n && (currentTs < startNs || currentTs > endNs)) 127 ) { 128 if (reverse) { 129 const [smaller] = searchSegment(this._results.tses, endNs); 130 // If there is no item in the viewport just go to the previous. 131 if (smaller === -1) { 132 this.setResultIndexWithSaturation(index - 1); 133 } else { 134 this._resultIndex = smaller; 135 } 136 } else { 137 const [, larger] = searchSegment(this._results.tses, startNs); 138 // If there is no item in the viewport just go to the next. 139 if (larger === -1) { 140 this.setResultIndexWithSaturation(index + 1); 141 } else { 142 this._resultIndex = larger; 143 } 144 } 145 } else { 146 // If the currentTs is in the viewport, increment the index. 147 if (reverse) { 148 this.setResultIndexWithSaturation(index - 1); 149 } else { 150 this.setResultIndexWithSaturation(index + 1); 151 } 152 } 153 if (this._onResultStep) { 154 this._onResultStep({ 155 eventId: this._results.eventIds[this._resultIndex], 156 ts: Time.fromRaw(this._results.tses[this._resultIndex]), 157 trackUri: this._results.trackUris[this._resultIndex], 158 source: this._results.sources[this._resultIndex], 159 }); 160 } 161 raf.scheduleFullRedraw(); 162 } 163 164 private setResultIndexWithSaturation(nextIndex: number) { 165 const tot = assertExists(this._results).totalResults; 166 assertTrue(tot !== 0); // The early out in step() guarantees this. 167 // This is a modulo operation that works also for negative numbers. 168 this._resultIndex = ((nextIndex % tot) + tot) % tot; 169 } 170 171 get hasResults() { 172 return this._results !== undefined; 173 } 174 175 get searchResults() { 176 return this._results; 177 } 178 179 get resultIndex() { 180 return this._resultIndex; 181 } 182 183 get searchText() { 184 return this._searchText; 185 } 186 187 get searchGeneration() { 188 return this._searchGeneration; 189 } 190 191 get searchInProgress(): boolean { 192 return this._searchInProgress; 193 } 194 195 private async executeSearch() { 196 const search = this._searchText; 197 const window = this._searchWindow; 198 const searchLiteral = escapeSearchQuery(this._searchText); 199 const generation = this._searchGeneration; 200 201 const engine = this._engine; 202 const trackManager = this._trackManager; 203 const workspace = this._workspace; 204 if (!engine || !trackManager || !workspace || !window) { 205 return; 206 } 207 208 // TODO(stevegolton): Avoid recomputing these indexes each time. 209 const trackUrisByCpu = new Map<number, string>(); 210 const allTracks = trackManager.getAllTracks(); 211 allTracks.forEach((td) => { 212 const tags = td?.tags; 213 const cpu = tags?.cpu; 214 const kind = tags?.kind; 215 exists(cpu) && 216 kind === CPU_SLICE_TRACK_KIND && 217 trackUrisByCpu.set(cpu, td.uri); 218 }); 219 220 const trackUrisByTrackId = new Map<number, string>(); 221 allTracks.forEach((td) => { 222 const trackIds = td?.tags?.trackIds ?? []; 223 trackIds.forEach((trackId) => trackUrisByTrackId.set(trackId, td.uri)); 224 }); 225 226 const utidRes = await engine.query(`select utid from thread join process 227 using(upid) where 228 thread.name glob ${searchLiteral} or 229 process.name glob ${searchLiteral}`); 230 const utids = []; 231 for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) { 232 utids.push(it.utid); 233 } 234 235 const res = await engine.query(` 236 select 237 id as sliceId, 238 ts, 239 'cpu' as source, 240 cpu as sourceId, 241 utid 242 from sched where utid in (${utids.join(',')}) 243 union all 244 select * 245 from ( 246 select 247 slice_id as sliceId, 248 ts, 249 'slice' as source, 250 track_id as sourceId, 251 0 as utid 252 from slice 253 where slice.name glob ${searchLiteral} 254 or ( 255 0 != CAST(${sqliteString(search)} AS INT) and 256 sliceId = CAST(${sqliteString(search)} AS INT) 257 ) 258 union 259 select 260 slice_id as sliceId, 261 ts, 262 'slice' as source, 263 track_id as sourceId, 264 0 as utid 265 from slice 266 join args using(arg_set_id) 267 where string_value glob ${searchLiteral} or key glob ${searchLiteral} 268 ) 269 union all 270 select 271 id as sliceId, 272 ts, 273 'log' as source, 274 0 as sourceId, 275 utid 276 from android_logs where msg glob ${searchLiteral} 277 order by ts 278 `); 279 280 const searchResults: SearchResults = { 281 eventIds: new Float64Array(0), 282 tses: new BigInt64Array(0), 283 utids: new Float64Array(0), 284 sources: [], 285 trackUris: [], 286 totalResults: 0, 287 }; 288 289 const lowerSearch = search.toLowerCase(); 290 for (const track of workspace.flatTracksOrdered) { 291 // We don't support searching for tracks that don't have a URI. 292 if (!track.uri) continue; 293 if (track.title.toLowerCase().indexOf(lowerSearch) === -1) { 294 continue; 295 } 296 searchResults.totalResults++; 297 searchResults.sources.push('track'); 298 searchResults.trackUris.push(track.uri); 299 } 300 301 const rows = res.numRows(); 302 searchResults.eventIds = new Float64Array( 303 searchResults.totalResults + rows, 304 ); 305 searchResults.tses = new BigInt64Array(searchResults.totalResults + rows); 306 searchResults.utids = new Float64Array(searchResults.totalResults + rows); 307 for (let i = 0; i < searchResults.totalResults; ++i) { 308 searchResults.eventIds[i] = -1; 309 searchResults.tses[i] = -1n; 310 searchResults.utids[i] = -1; 311 } 312 313 const it = res.iter({ 314 sliceId: NUM, 315 ts: LONG, 316 source: STR, 317 sourceId: NUM, 318 utid: NUM, 319 }); 320 for (; it.valid(); it.next()) { 321 let track: string | undefined = undefined; 322 323 if (it.source === 'cpu') { 324 track = trackUrisByCpu.get(it.sourceId); 325 } else if (it.source === 'slice') { 326 track = trackUrisByTrackId.get(it.sourceId); 327 } else if (it.source === 'log') { 328 track = trackManager 329 .getAllTracks() 330 .find((td) => td.tags?.kind === ANDROID_LOGS_TRACK_KIND)?.uri; 331 } 332 // The .get() calls above could return undefined, this isn't just an else. 333 if (track === undefined) { 334 continue; 335 } 336 337 const i = searchResults.totalResults++; 338 searchResults.trackUris.push(track); 339 searchResults.sources.push(it.source as SearchSource); 340 searchResults.eventIds[i] = it.sliceId; 341 searchResults.tses[i] = it.ts; 342 searchResults.utids[i] = it.utid; 343 } 344 345 if (generation !== this._searchGeneration) { 346 // We arrived too late. By the time we computed results the user issued 347 // another search. 348 return; 349 } 350 this._results = searchResults; 351 this._resultIndex = -1; 352 } 353} 354