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