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 {TRACE_MARGIN_TIME_S} from '../common/constants'; 16import {Engine} from '../common/engine'; 17import {NUM, STR} from '../common/query_result'; 18import {CurrentSearchResults, SearchSummary} from '../common/search_data'; 19import {TimeSpan} from '../common/time'; 20import {publishSearch, publishSearchResult} from '../frontend/publish'; 21 22import {Controller} from './controller'; 23import {App} from './globals'; 24 25export function escapeQuery(s: string): string { 26 // See https://www.sqlite.org/lang_expr.html#:~:text=A%20string%20constant 27 s = s.replace(/\'/g, '\'\''); 28 s = s.replace(/_/g, '^_'); 29 s = s.replace(/%/g, '^%'); 30 return `'%${s}%' escape '^'`; 31} 32 33export interface SearchControllerArgs { 34 engine: Engine; 35 app: App; 36} 37 38export class SearchController extends Controller<'main'> { 39 private engine: Engine; 40 private app: App; 41 private previousSpan: TimeSpan; 42 private previousResolution: number; 43 private previousSearch: string; 44 private updateInProgress: boolean; 45 private setupInProgress: boolean; 46 47 constructor(args: SearchControllerArgs) { 48 super('main'); 49 this.engine = args.engine; 50 this.app = args.app; 51 this.previousSpan = new TimeSpan(0, 1); 52 this.previousSearch = ''; 53 this.updateInProgress = false; 54 this.setupInProgress = true; 55 this.previousResolution = 1; 56 this.setup().finally(() => { 57 this.setupInProgress = false; 58 this.run(); 59 }); 60 } 61 62 private async setup() { 63 await this.query(`create virtual table search_summary_window 64 using window;`); 65 await this.query(`create virtual table search_summary_sched_span using 66 span_join(sched PARTITIONED cpu, search_summary_window);`); 67 await this.query(`create virtual table search_summary_slice_span using 68 span_join(slice PARTITIONED track_id, search_summary_window);`); 69 } 70 71 run() { 72 if (this.setupInProgress || this.updateInProgress) { 73 return; 74 } 75 76 const visibleState = this.app.state.frontendLocalState.visibleState; 77 const omniboxState = this.app.state.frontendLocalState.omniboxState; 78 if (visibleState === undefined || omniboxState === undefined || 79 omniboxState.mode === 'COMMAND') { 80 return; 81 } 82 const newSpan = new TimeSpan(visibleState.startSec, visibleState.endSec); 83 const newSearch = omniboxState.omnibox; 84 let newResolution = visibleState.resolution; 85 if (this.previousSpan.contains(newSpan) && 86 this.previousResolution === newResolution && 87 newSearch === this.previousSearch) { 88 return; 89 } 90 this.previousSpan = new TimeSpan( 91 Math.max(newSpan.start - newSpan.duration, -TRACE_MARGIN_TIME_S), 92 newSpan.end + newSpan.duration); 93 this.previousResolution = newResolution; 94 this.previousSearch = newSearch; 95 if (newSearch === '' || newSearch.length < 4) { 96 publishSearch({ 97 tsStarts: new Float64Array(0), 98 tsEnds: new Float64Array(0), 99 count: new Uint8Array(0), 100 }); 101 publishSearchResult({ 102 sliceIds: new Float64Array(0), 103 tsStarts: new Float64Array(0), 104 utids: new Float64Array(0), 105 sources: [], 106 trackIds: [], 107 totalResults: 0, 108 }); 109 return; 110 } 111 112 let startNs = Math.round(newSpan.start * 1e9); 113 let endNs = Math.round(newSpan.end * 1e9); 114 115 // TODO(hjd): We shouldn't need to be so defensive here: 116 if (!Number.isFinite(startNs)) { 117 startNs = 0; 118 } 119 if (!Number.isFinite(endNs)) { 120 endNs = 1; 121 } 122 if (!Number.isFinite(newResolution)) { 123 newResolution = 1; 124 } 125 126 this.updateInProgress = true; 127 const computeSummary = 128 this.update(newSearch, startNs, endNs, newResolution).then(summary => { 129 publishSearch(summary); 130 }); 131 132 const computeResults = 133 this.specificSearch(newSearch).then(searchResults => { 134 publishSearchResult(searchResults); 135 }); 136 137 Promise.all([computeSummary, computeResults]) 138 .finally(() => { 139 this.updateInProgress = false; 140 this.run(); 141 }); 142 } 143 144 onDestroy() {} 145 146 private async update( 147 search: string, startNs: number, endNs: number, 148 resolution: number): Promise<SearchSummary> { 149 const quantumNs = Math.round(resolution * 10 * 1e9); 150 151 const searchLiteral = escapeQuery(search); 152 153 startNs = Math.floor(startNs / quantumNs) * quantumNs; 154 155 const windowDur = Math.max(endNs - startNs, 1); 156 await this.query(`update search_summary_window set 157 window_start=${startNs}, 158 window_dur=${windowDur}, 159 quantum=${quantumNs} 160 where rowid = 0;`); 161 162 const utidRes = await this.query(`select utid from thread join process 163 using(upid) where thread.name like ${searchLiteral} 164 or process.name like ${searchLiteral}`); 165 166 const utids = []; 167 for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) { 168 utids.push(it.utid); 169 } 170 171 const cpus = await this.engine.getCpus(); 172 const maxCpu = Math.max(...cpus, -1); 173 174 const res = await this.query(` 175 select 176 (quantum_ts * ${quantumNs} + ${startNs})/1e9 as tsStart, 177 ((quantum_ts+1) * ${quantumNs} + ${startNs})/1e9 as tsEnd, 178 min(count(*), 255) as count 179 from ( 180 select 181 quantum_ts 182 from search_summary_sched_span 183 where utid in (${utids.join(',')}) and cpu <= ${maxCpu} 184 union all 185 select 186 quantum_ts 187 from search_summary_slice_span 188 where name like ${searchLiteral} 189 ) 190 group by quantum_ts 191 order by quantum_ts;`); 192 193 const numRows = res.numRows(); 194 const summary = { 195 tsStarts: new Float64Array(numRows), 196 tsEnds: new Float64Array(numRows), 197 count: new Uint8Array(numRows) 198 }; 199 200 const it = res.iter({tsStart: NUM, tsEnd: NUM, count: NUM}); 201 for (let row = 0; it.valid(); it.next(), ++row) { 202 summary.tsStarts[row] = it.tsStart; 203 summary.tsEnds[row] = it.tsEnd; 204 summary.count[row] = it.count; 205 } 206 return summary; 207 } 208 209 private async specificSearch(search: string) { 210 const searchLiteral = escapeQuery(search); 211 // TODO(hjd): we should avoid recomputing this every time. This will be 212 // easier once the track table has entries for all the tracks. 213 const cpuToTrackId = new Map(); 214 const engineTrackIdToTrackId = new Map(); 215 for (const track of Object.values(this.app.state.tracks)) { 216 if (track.kind === 'CpuSliceTrack') { 217 cpuToTrackId.set((track.config as {cpu: number}).cpu, track.id); 218 continue; 219 } 220 const config = track.config || {}; 221 if (config.trackId !== undefined) { 222 engineTrackIdToTrackId.set(config.trackId, track.id); 223 continue; 224 } 225 if (config.trackIds !== undefined) { 226 for (const trackId of config.trackIds) { 227 engineTrackIdToTrackId.set(trackId, track.id); 228 } 229 continue; 230 } 231 } 232 233 const utidRes = await this.query(`select utid from thread join process 234 using(upid) where 235 thread.name like ${searchLiteral} or 236 process.name like ${searchLiteral}`); 237 const utids = []; 238 for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) { 239 utids.push(it.utid); 240 } 241 242 const queryRes = await this.query(` 243 select 244 id as sliceId, 245 ts, 246 'cpu' as source, 247 cpu as sourceId, 248 utid 249 from sched where utid in (${utids.join(',')}) 250 union 251 select 252 slice_id as sliceId, 253 ts, 254 'track' as source, 255 track_id as sourceId, 256 0 as utid 257 from slice 258 where slice.name like ${searchLiteral} 259 ${isNaN(Number(search)) ? '' : `or sliceId = ${search}`} 260 union 261 select 262 slice_id as sliceId, 263 ts, 264 'track' as source, 265 track_id as sourceId, 266 0 as utid 267 from slice 268 join args using(arg_set_id) 269 where string_value like ${searchLiteral} 270 order by ts`); 271 272 const rows = queryRes.numRows(); 273 const searchResults: CurrentSearchResults = { 274 sliceIds: new Float64Array(rows), 275 tsStarts: new Float64Array(rows), 276 utids: new Float64Array(rows), 277 trackIds: [], 278 sources: [], 279 totalResults: 0, 280 }; 281 282 const it = queryRes.iter( 283 {sliceId: NUM, ts: NUM, source: STR, sourceId: NUM, utid: NUM}); 284 for (; it.valid(); it.next()) { 285 let trackId = undefined; 286 if (it.source === 'cpu') { 287 trackId = cpuToTrackId.get(it.sourceId); 288 } else if (it.source === 'track') { 289 trackId = engineTrackIdToTrackId.get(it.sourceId); 290 } 291 292 // The .get() calls above could return undefined, this isn't just an else. 293 if (trackId === undefined) { 294 continue; 295 } 296 297 const i = searchResults.totalResults++; 298 searchResults.trackIds.push(trackId); 299 searchResults.sources.push(it.source); 300 searchResults.sliceIds[i] = it.sliceId; 301 searchResults.tsStarts[i] = it.ts; 302 searchResults.utids[i] = it.utid; 303 } 304 return searchResults; 305 } 306 307 private async query(query: string) { 308 const result = await this.engine.query(query); 309 return result; 310 } 311} 312