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 {BigintMath} from '../base/bigint_math'; 16import {sqliteString} from '../base/string_utils'; 17import {Engine} from '../common/engine'; 18import {NUM, STR} from '../common/query_result'; 19import {escapeSearchQuery} from '../common/query_utils'; 20import {CurrentSearchResults, SearchSummary} from '../common/search_data'; 21import {Span} from '../common/time'; 22import { 23 TPDuration, 24 TPTime, 25 TPTimeSpan, 26} from '../common/time'; 27import {globals} from '../frontend/globals'; 28import {publishSearch, publishSearchResult} from '../frontend/publish'; 29 30import {Controller} from './controller'; 31 32export interface SearchControllerArgs { 33 engine: Engine; 34} 35 36export class SearchController extends Controller<'main'> { 37 private engine: Engine; 38 private previousSpan: Span<TPTime>; 39 private previousResolution: TPDuration; 40 private previousSearch: string; 41 private updateInProgress: boolean; 42 private setupInProgress: boolean; 43 44 constructor(args: SearchControllerArgs) { 45 super('main'); 46 this.engine = args.engine; 47 this.previousSpan = new TPTimeSpan(0n, 1n); 48 this.previousSearch = ''; 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 (visibleState === undefined || omniboxState === undefined || 75 omniboxState.mode === 'COMMAND') { 76 return; 77 } 78 const newSpan = globals.stateVisibleTime(); 79 const newSearch = omniboxState.omnibox; 80 const newResolution = visibleState.resolution; 81 if (this.previousSpan.contains(newSpan) && 82 this.previousResolution === newResolution && 83 newSearch === this.previousSearch) { 84 return; 85 } 86 87 88 // TODO(hjd): We should restrict this to the start of the trace but 89 // that is not easily available here. 90 // N.B. Timestamps can be negative. 91 const {start, end} = newSpan.pad(newSpan.duration); 92 this.previousSpan = new TPTimeSpan(start, end); 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 this.updateInProgress = true; 113 const computeSummary = 114 this.update(newSearch, newSpan.start, newSpan.end, newResolution) 115 .then((summary) => { 116 publishSearch(summary); 117 }); 118 119 const computeResults = 120 this.specificSearch(newSearch).then((searchResults) => { 121 publishSearchResult(searchResults); 122 }); 123 124 Promise.all([computeSummary, computeResults]) 125 .finally(() => { 126 this.updateInProgress = false; 127 this.run(); 128 }); 129 } 130 131 onDestroy() {} 132 133 private async update( 134 search: string, startNs: TPTime, endNs: TPTime, 135 resolution: TPDuration): Promise<SearchSummary> { 136 const searchLiteral = escapeSearchQuery(search); 137 138 const quantumNs = resolution * 10n; 139 startNs = BigintMath.quantizeFloor(startNs, quantumNs); 140 141 const windowDur = BigintMath.max(endNs - startNs, 1n); 142 await this.query(`update search_summary_window set 143 window_start=${startNs}, 144 window_dur=${windowDur}, 145 quantum=${quantumNs} 146 where rowid = 0;`); 147 148 const utidRes = await this.query(`select utid from thread join process 149 using(upid) where thread.name glob ${searchLiteral} 150 or process.name glob ${searchLiteral}`); 151 152 const utids = []; 153 for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) { 154 utids.push(it.utid); 155 } 156 157 const cpus = await this.engine.getCpus(); 158 const maxCpu = Math.max(...cpus, -1); 159 160 const res = await this.query(` 161 select 162 (quantum_ts * ${quantumNs} + ${startNs})/1e9 as tsStart, 163 ((quantum_ts+1) * ${quantumNs} + ${startNs})/1e9 as tsEnd, 164 min(count(*), 255) as count 165 from ( 166 select 167 quantum_ts 168 from search_summary_sched_span 169 where utid in (${utids.join(',')}) and cpu <= ${maxCpu} 170 union all 171 select 172 quantum_ts 173 from search_summary_slice_span 174 where name glob ${searchLiteral} 175 ) 176 group by quantum_ts 177 order by quantum_ts;`); 178 179 const numRows = res.numRows(); 180 const summary = { 181 tsStarts: new Float64Array(numRows), 182 tsEnds: new Float64Array(numRows), 183 count: new Uint8Array(numRows), 184 }; 185 186 const it = res.iter({tsStart: NUM, tsEnd: NUM, count: NUM}); 187 for (let row = 0; it.valid(); it.next(), ++row) { 188 summary.tsStarts[row] = it.tsStart; 189 summary.tsEnds[row] = it.tsEnd; 190 summary.count[row] = it.count; 191 } 192 return summary; 193 } 194 195 private async specificSearch(search: string) { 196 const searchLiteral = escapeSearchQuery(search); 197 // TODO(hjd): we should avoid recomputing this every time. This will be 198 // easier once the track table has entries for all the tracks. 199 const cpuToTrackId = new Map(); 200 for (const track of Object.values(globals.state.tracks)) { 201 if (track.kind === 'CpuSliceTrack') { 202 cpuToTrackId.set((track.config as {cpu: number}).cpu, track.id); 203 continue; 204 } 205 } 206 207 const utidRes = await this.query(`select utid from thread join process 208 using(upid) where 209 thread.name glob ${searchLiteral} or 210 process.name glob ${searchLiteral}`); 211 const utids = []; 212 for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) { 213 utids.push(it.utid); 214 } 215 216 const queryRes = await this.query(` 217 select 218 id as sliceId, 219 ts, 220 'cpu' as source, 221 cpu as sourceId, 222 utid 223 from sched where utid in (${utids.join(',')}) 224 union 225 select 226 slice_id as sliceId, 227 ts, 228 'track' as source, 229 track_id as sourceId, 230 0 as utid 231 from slice 232 where slice.name glob ${searchLiteral} 233 or ( 234 0 != CAST(${(sqliteString(search))} AS INT) and 235 sliceId = CAST(${(sqliteString(search))} AS INT) 236 ) 237 union 238 select 239 slice_id as sliceId, 240 ts, 241 'track' as source, 242 track_id as sourceId, 243 0 as utid 244 from slice 245 join args using(arg_set_id) 246 where string_value glob ${searchLiteral} or key glob ${searchLiteral} 247 union 248 select 249 id as sliceId, 250 ts, 251 'log' as source, 252 0 as sourceId, 253 utid 254 from android_logs where msg glob ${searchLiteral} 255 order by ts 256 257 `); 258 259 const rows = queryRes.numRows(); 260 const searchResults: CurrentSearchResults = { 261 sliceIds: new Float64Array(rows), 262 tsStarts: new Float64Array(rows), 263 utids: new Float64Array(rows), 264 trackIds: [], 265 sources: [], 266 totalResults: 0, 267 }; 268 269 const it = queryRes.iter( 270 {sliceId: NUM, ts: NUM, source: STR, sourceId: NUM, utid: NUM}); 271 for (; it.valid(); it.next()) { 272 let trackId = undefined; 273 if (it.source === 'cpu') { 274 trackId = cpuToTrackId.get(it.sourceId); 275 } else if (it.source === 'track') { 276 trackId = globals.state.uiTrackIdByTraceTrackId[it.sourceId]; 277 } else if (it.source === 'log') { 278 const logTracks = Object.values(globals.state.tracks) 279 .filter((t) => t.kind === 'AndroidLogTrack'); 280 if (logTracks.length > 0) { 281 trackId = logTracks[0].id; 282 } 283 } 284 285 // The .get() calls above could return undefined, this isn't just an else. 286 if (trackId === undefined) { 287 continue; 288 } 289 290 const i = searchResults.totalResults++; 291 searchResults.trackIds.push(trackId); 292 searchResults.sources.push(it.source); 293 searchResults.sliceIds[i] = it.sliceId; 294 searchResults.tsStarts[i] = it.ts; 295 searchResults.utids[i] = it.utid; 296 } 297 return searchResults; 298 } 299 300 private async query(query: string) { 301 const result = await this.engine.query(query); 302 return result; 303 } 304} 305