xref: /aosp_15_r20/external/perfetto/ui/src/frontend/search_overview_track.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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