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 size 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 {AsyncDisposableStack} from '../base/disposable_stack'; 17import {Size2D} from '../base/geom'; 18import {Duration, Time, TimeSpan, duration, time} from '../base/time'; 19import {TimeScale} from '../base/time_scale'; 20import {calculateResolution} from '../common/resolution'; 21import {TraceImpl} from '../core/trace_impl'; 22import {LONG, NUM} from '../trace_processor/query_result'; 23import {escapeSearchQuery} from '../trace_processor/query_utils'; 24import {createVirtualTable} from '../trace_processor/sql_utils'; 25 26interface SearchSummary { 27 tsStarts: BigInt64Array; 28 tsEnds: BigInt64Array; 29 count: Uint8Array; 30} 31 32/** 33 * This component is drawn on top of the timeline and creates small yellow 34 * rectangles that highlight the time span of search results (similarly to what 35 * Chrome does on the scrollbar when you Ctrl+F and type a search term). 36 * It reacts to changes in SearchManager and queries the quantized ranges of the 37 * search results. 38 */ 39export class SearchOverviewTrack implements AsyncDisposable { 40 private readonly trash = new AsyncDisposableStack(); 41 private readonly trace: TraceImpl; 42 private readonly limiter = new AsyncLimiter(); 43 private initialized = false; 44 private previousResolution: duration | undefined; 45 private previousSpan: TimeSpan | undefined; 46 private previousSearchGeneration = 0; 47 private searchSummary: SearchSummary | undefined; 48 49 constructor(trace: TraceImpl) { 50 this.trace = trace; 51 } 52 53 render(ctx: CanvasRenderingContext2D, size: Size2D) { 54 this.maybeUpdate(size); 55 this.renderSearchOverview(ctx, size); 56 } 57 58 private async initialize() { 59 const engine = this.trace.engine; 60 this.trash.use( 61 await createVirtualTable(engine, 'search_summary_window', 'window'), 62 ); 63 this.trash.use( 64 await createVirtualTable( 65 engine, 66 'search_summary_sched_span', 67 'span_join(sched PARTITIONED cpu, search_summary_window)', 68 ), 69 ); 70 this.trash.use( 71 await createVirtualTable( 72 engine, 73 'search_summary_slice_span', 74 'span_join(slice PARTITIONED track_id, search_summary_window)', 75 ), 76 ); 77 } 78 79 private async update( 80 search: string, 81 start: time, 82 end: time, 83 resolution: duration, 84 ): Promise<SearchSummary> { 85 if (!this.initialized) { 86 this.initialized = true; 87 await this.initialize(); 88 } 89 const searchLiteral = escapeSearchQuery(search); 90 91 const resolutionScalingFactor = 10n; 92 const quantum = resolution * resolutionScalingFactor; 93 start = Time.quantFloor(start, quantum); 94 95 const windowDur = Duration.max(Time.diff(end, start), 1n); 96 const engine = this.trace.engine; 97 await engine.query(`update search_summary_window set 98 window_start=${start}, 99 window_dur=${windowDur}, 100 quantum=${quantum} 101 where rowid = 0;`); 102 103 const utidRes = await engine.query(`select utid from thread join process 104 using(upid) where thread.name glob ${searchLiteral} 105 or process.name glob ${searchLiteral}`); 106 107 const utids = []; 108 for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) { 109 utids.push(it.utid); 110 } 111 112 const res = await engine.query(` 113 select 114 (quantum_ts * ${quantum} + ${start}) as tsStart, 115 ((quantum_ts+1) * ${quantum} + ${start}) as tsEnd, 116 min(count(*), 255) as count 117 from ( 118 select 119 quantum_ts 120 from search_summary_sched_span 121 where utid in (${utids.join(',')}) 122 union all 123 select 124 quantum_ts 125 from search_summary_slice_span 126 where name glob ${searchLiteral} 127 ) 128 group by quantum_ts 129 order by quantum_ts;`); 130 131 const numRows = res.numRows(); 132 const summary: SearchSummary = { 133 tsStarts: new BigInt64Array(numRows), 134 tsEnds: new BigInt64Array(numRows), 135 count: new Uint8Array(numRows), 136 }; 137 138 const it = res.iter({tsStart: LONG, tsEnd: LONG, count: NUM}); 139 for (let row = 0; it.valid(); it.next(), ++row) { 140 summary.tsStarts[row] = it.tsStart; 141 summary.tsEnds[row] = it.tsEnd; 142 summary.count[row] = it.count; 143 } 144 return summary; 145 } 146 147 private maybeUpdate(size: Size2D) { 148 const searchManager = this.trace.search; 149 const timeline = this.trace.timeline; 150 if (!searchManager.hasResults) { 151 return; 152 } 153 const newSpan = timeline.visibleWindow; 154 const newSearchGeneration = searchManager.searchGeneration; 155 const newResolution = calculateResolution(newSpan, size.width); 156 const newTimeSpan = newSpan.toTimeSpan(); 157 if ( 158 this.previousSpan?.containsSpan(newTimeSpan.start, newTimeSpan.end) && 159 this.previousResolution === newResolution && 160 this.previousSearchGeneration === newSearchGeneration 161 ) { 162 return; 163 } 164 165 // TODO(hjd): We should restrict this to the start of the trace but 166 // that is not easily available here. 167 // N.B. Timestamps can be negative. 168 const {start, end} = newTimeSpan.pad(newTimeSpan.duration); 169 this.previousSpan = new TimeSpan(start, end); 170 this.previousResolution = newResolution; 171 this.previousSearchGeneration = newSearchGeneration; 172 const search = searchManager.searchText; 173 if (search === '') { 174 this.searchSummary = { 175 tsStarts: new BigInt64Array(0), 176 tsEnds: new BigInt64Array(0), 177 count: new Uint8Array(0), 178 }; 179 return; 180 } 181 182 this.limiter.schedule(async () => { 183 const summary = await this.update( 184 searchManager.searchText, 185 start, 186 end, 187 newResolution, 188 ); 189 this.searchSummary = summary; 190 }); 191 } 192 193 private renderSearchOverview( 194 ctx: CanvasRenderingContext2D, 195 size: Size2D, 196 ): void { 197 const visibleWindow = this.trace.timeline.visibleWindow; 198 const timescale = new TimeScale(visibleWindow, { 199 left: 0, 200 right: size.width, 201 }); 202 203 if (!this.searchSummary) return; 204 205 for (let i = 0; i < this.searchSummary.tsStarts.length; i++) { 206 const tStart = Time.fromRaw(this.searchSummary.tsStarts[i]); 207 const tEnd = Time.fromRaw(this.searchSummary.tsEnds[i]); 208 if (!visibleWindow.overlaps(tStart, tEnd)) { 209 continue; 210 } 211 const rectStart = Math.max(timescale.timeToPx(tStart), 0); 212 const rectEnd = timescale.timeToPx(tEnd); 213 ctx.fillStyle = '#ffe263'; 214 ctx.fillRect( 215 Math.floor(rectStart), 216 0, 217 Math.ceil(rectEnd - rectStart), 218 size.height, 219 ); 220 } 221 const results = this.trace.search.searchResults; 222 if (results === undefined) { 223 return; 224 } 225 const index = this.trace.search.resultIndex; 226 if (index !== -1 && index < results.tses.length) { 227 const start = results.tses[index]; 228 if (start !== -1n) { 229 const triangleStart = Math.max( 230 timescale.timeToPx(Time.fromRaw(start)), 231 0, 232 ); 233 ctx.fillStyle = '#000'; 234 ctx.beginPath(); 235 ctx.moveTo(triangleStart, size.height); 236 ctx.lineTo(triangleStart - 3, 0); 237 ctx.lineTo(triangleStart + 3, 0); 238 ctx.lineTo(triangleStart, size.height); 239 ctx.fill(); 240 ctx.closePath(); 241 } 242 } 243 244 ctx.restore(); 245 } 246 247 async [Symbol.asyncDispose](): Promise<void> { 248 return await this.trash.asyncDispose(); 249 } 250} 251