• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 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 {AsyncLimiter} from '../base/async_limiter';
16import {sqliteString} from '../base/string_utils';
17import {Time} from '../base/time';
18import {exists} from '../base/utils';
19import {ResultStepEventHandler} from '../public/search';
20import {
21  ANDROID_LOGS_TRACK_KIND,
22  CPU_SLICE_TRACK_KIND,
23} from '../public/track_kinds';
24import {Workspace} from '../public/workspace';
25import {SourceDataset, UnionDataset} from '../trace_processor/dataset';
26import {Engine} from '../trace_processor/engine';
27import {LONG, NUM, STR} from '../trace_processor/query_result';
28import {escapeSearchQuery} from '../trace_processor/query_utils';
29import {featureFlags} from './feature_flags';
30import {raf} from './raf_scheduler';
31import {SearchSource} from './search_data';
32import {TimelineImpl} from './timeline';
33import {TrackManagerImpl} from './track_manager';
34
35const DATASET_SEARCH = featureFlags.register({
36  id: 'datasetSearch',
37  name: 'Use dataset search',
38  description:
39    '[Experimental] use dataset for search, which allows searching all tracks with a matching dataset. Might be slower than normal search.',
40  defaultValue: false,
41});
42
43export interface SearchResults {
44  eventIds: Float64Array;
45  tses: BigInt64Array;
46  utids: Float64Array;
47  trackUris: string[];
48  sources: SearchSource[];
49  totalResults: number;
50}
51
52export class SearchManagerImpl {
53  private _searchGeneration = 0;
54  private _searchText = '';
55  private _results?: SearchResults;
56  private _resultIndex = -1;
57  private _searchInProgress = false;
58
59  // TODO(primiano): once we get rid of globals, these below can be made always
60  // defined. the ?: is to deal with globals-before-trace-load.
61  private _timeline?: TimelineImpl;
62  private _trackManager?: TrackManagerImpl;
63  private _workspace?: Workspace;
64  private _engine?: Engine;
65  private _limiter = new AsyncLimiter();
66  private _onResultStep?: ResultStepEventHandler;
67
68  constructor(args?: {
69    timeline: TimelineImpl;
70    trackManager: TrackManagerImpl;
71    workspace: Workspace;
72    engine: Engine;
73    onResultStep: ResultStepEventHandler;
74  }) {
75    this._timeline = args?.timeline;
76    this._trackManager = args?.trackManager;
77    this._engine = args?.engine;
78    this._workspace = args?.workspace;
79    this._onResultStep = args?.onResultStep;
80  }
81
82  search(text: string) {
83    if (text === this._searchText) {
84      return;
85    }
86    this._searchText = text;
87    this._searchGeneration++;
88    this._results = undefined;
89    this._resultIndex = -1;
90    this._searchInProgress = false;
91    if (text !== '') {
92      this._searchInProgress = true;
93      this._limiter.schedule(async () => {
94        if (DATASET_SEARCH.get()) {
95          await this.executeDatasetSearch();
96        } else {
97          await this.executeSearch();
98        }
99        this._searchInProgress = false;
100        raf.scheduleFullRedraw();
101      });
102    }
103  }
104
105  reset() {
106    this.search('');
107  }
108
109  stepForward() {
110    this.stepInternal(false);
111  }
112
113  stepBackwards() {
114    this.stepInternal(true);
115  }
116
117  private stepInternal(reverse = false) {
118    if (this._results === undefined) return;
119
120    // If the value of |this._results.totalResults| is 0,
121    // it means that the query is in progress or no results are found.
122    if (this._results.totalResults === 0) {
123      return;
124    }
125
126    if (reverse) {
127      --this._resultIndex;
128      if (this._resultIndex < 0) {
129        this._resultIndex = this._results.totalResults - 1;
130      }
131    } else {
132      ++this._resultIndex;
133      if (this._resultIndex > this._results.totalResults - 1) {
134        this._resultIndex = 0;
135      }
136    }
137    this._onResultStep?.({
138      eventId: this._results.eventIds[this._resultIndex],
139      ts: Time.fromRaw(this._results.tses[this._resultIndex]),
140      trackUri: this._results.trackUris[this._resultIndex],
141      source: this._results.sources[this._resultIndex],
142    });
143  }
144
145  get hasResults() {
146    return this._results !== undefined;
147  }
148
149  get searchResults() {
150    return this._results;
151  }
152
153  get resultIndex() {
154    return this._resultIndex;
155  }
156
157  get searchText() {
158    return this._searchText;
159  }
160
161  get searchGeneration() {
162    return this._searchGeneration;
163  }
164
165  get searchInProgress(): boolean {
166    return this._searchInProgress;
167  }
168
169  private async executeSearch() {
170    const search = this._searchText;
171    const searchLiteral = escapeSearchQuery(this._searchText);
172    const generation = this._searchGeneration;
173
174    const engine = this._engine;
175    const trackManager = this._trackManager;
176    const workspace = this._workspace;
177    if (!engine || !trackManager || !workspace) {
178      return;
179    }
180
181    // TODO(stevegolton): Avoid recomputing these indexes each time.
182    const trackUrisByCpu = new Map<number, string>();
183    const allTracks = trackManager.getAllTracks();
184    allTracks.forEach((td) => {
185      const tags = td?.tags;
186      const cpu = tags?.cpu;
187      const kind = tags?.kind;
188      exists(cpu) &&
189        kind === CPU_SLICE_TRACK_KIND &&
190        trackUrisByCpu.set(cpu, td.uri);
191    });
192
193    const trackUrisByTrackId = new Map<number, string>();
194    allTracks.forEach((td) => {
195      const trackIds = td?.tags?.trackIds ?? [];
196      trackIds.forEach((trackId) => trackUrisByTrackId.set(trackId, td.uri));
197    });
198
199    const utidRes = await engine.query(`select utid from thread join process
200    using(upid) where
201      thread.name glob ${searchLiteral} or
202      process.name glob ${searchLiteral}`);
203    const utids = [];
204    for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
205      utids.push(it.utid);
206    }
207
208    const res = await engine.query(`
209      select
210        id as sliceId,
211        ts,
212        'cpu' as source,
213        cpu as sourceId,
214        utid
215      from sched where utid in (${utids.join(',')})
216      union all
217      select *
218      from (
219        select
220          slice_id as sliceId,
221          ts,
222          'slice' as source,
223          track_id as sourceId,
224          0 as utid
225          from slice
226          where slice.name glob ${searchLiteral}
227            or (
228              0 != CAST(${sqliteString(search)} AS INT) and
229              sliceId = CAST(${sqliteString(search)} AS INT)
230            )
231        union
232        select
233          slice_id as sliceId,
234          ts,
235          'slice' as source,
236          track_id as sourceId,
237          0 as utid
238        from slice
239        join args using(arg_set_id)
240        where string_value glob ${searchLiteral} or key glob ${searchLiteral}
241      )
242      union all
243      select
244        id as sliceId,
245        ts,
246        'log' as source,
247        0 as sourceId,
248        utid
249      from android_logs where msg glob ${searchLiteral}
250      order by ts
251    `);
252
253    const searchResults: SearchResults = {
254      eventIds: new Float64Array(0),
255      tses: new BigInt64Array(0),
256      utids: new Float64Array(0),
257      sources: [],
258      trackUris: [],
259      totalResults: 0,
260    };
261
262    const lowerSearch = search.toLowerCase();
263    for (const track of workspace.flatTracksOrdered) {
264      // We don't support searching for tracks that don't have a URI.
265      if (!track.uri) continue;
266      if (track.title.toLowerCase().indexOf(lowerSearch) === -1) {
267        continue;
268      }
269      searchResults.totalResults++;
270      searchResults.sources.push('track');
271      searchResults.trackUris.push(track.uri);
272    }
273
274    const rows = res.numRows();
275    searchResults.eventIds = new Float64Array(
276      searchResults.totalResults + rows,
277    );
278    searchResults.tses = new BigInt64Array(searchResults.totalResults + rows);
279    searchResults.utids = new Float64Array(searchResults.totalResults + rows);
280    for (let i = 0; i < searchResults.totalResults; ++i) {
281      searchResults.eventIds[i] = -1;
282      searchResults.tses[i] = -1n;
283      searchResults.utids[i] = -1;
284    }
285
286    const it = res.iter({
287      sliceId: NUM,
288      ts: LONG,
289      source: STR,
290      sourceId: NUM,
291      utid: NUM,
292    });
293    for (; it.valid(); it.next()) {
294      let track: string | undefined = undefined;
295
296      if (it.source === 'cpu') {
297        track = trackUrisByCpu.get(it.sourceId);
298      } else if (it.source === 'slice') {
299        track = trackUrisByTrackId.get(it.sourceId);
300      } else if (it.source === 'log') {
301        track = trackManager
302          .getAllTracks()
303          .find((td) => td.tags?.kind === ANDROID_LOGS_TRACK_KIND)?.uri;
304      }
305      // The .get() calls above could return undefined, this isn't just an else.
306      if (track === undefined) {
307        continue;
308      }
309
310      const i = searchResults.totalResults++;
311      searchResults.trackUris.push(track);
312      searchResults.sources.push(it.source as SearchSource);
313      searchResults.eventIds[i] = it.sliceId;
314      searchResults.tses[i] = it.ts;
315      searchResults.utids[i] = it.utid;
316    }
317
318    if (generation !== this._searchGeneration) {
319      // We arrived too late. By the time we computed results the user issued
320      // another search.
321      return;
322    }
323    this._results = searchResults;
324
325    // We have changed the search results - try and find the first result that's
326    // after the start of this visible window.
327    const visibleWindow = this._timeline?.visibleWindow.toTimeSpan();
328    if (visibleWindow) {
329      const foundIndex = this._results.tses.findIndex(
330        (ts) => ts >= visibleWindow.start,
331      );
332      if (foundIndex === -1) {
333        this._resultIndex = -1;
334      } else {
335        // Store the value before the found one, so that when the user presses
336        // enter we navigate to the correct one.
337        this._resultIndex = foundIndex - 1;
338      }
339    } else {
340      this._resultIndex = -1;
341    }
342  }
343
344  private async executeDatasetSearch() {
345    const trackManager = this._trackManager;
346    const engine = this._engine;
347    if (!engine || !trackManager) {
348      return;
349    }
350
351    const generation = this._searchGeneration;
352    const searchLiteral = escapeSearchQuery(this._searchText);
353
354    const datasets = trackManager
355      .getAllTracks()
356      .map((t) => {
357        const dataset = t.track.getDataset?.();
358        if (dataset) {
359          return [dataset, t.uri] as const;
360        } else {
361          return undefined;
362        }
363      })
364      .filter(exists)
365      .filter(([dataset]) => dataset.implements({id: NUM, ts: LONG, name: STR}))
366      .map(
367        ([dataset, uri]) =>
368          new SourceDataset({
369            src: `
370              select
371                id,
372                ts,
373                name,
374                '${uri}' as uri
375              from (${dataset.query()})`,
376            schema: {id: NUM, ts: LONG, name: STR, uri: STR},
377          }),
378      );
379
380    const union = new UnionDataset(datasets);
381    const result = await engine.query(`
382      select
383        id,
384        uri,
385        ts
386      from (${union.query()})
387      where name glob ${searchLiteral}
388    `);
389
390    const numRows = result.numRows();
391    const searchResults: SearchResults = {
392      eventIds: new Float64Array(numRows),
393      tses: new BigInt64Array(numRows),
394      utids: new Float64Array(numRows),
395      sources: [],
396      trackUris: [],
397      totalResults: numRows,
398    };
399
400    let i = 0;
401    for (
402      const iter = result.iter({id: NUM, ts: LONG, uri: STR});
403      iter.valid();
404      iter.next()
405    ) {
406      searchResults.eventIds[i] = iter.id;
407      searchResults.tses[i] = iter.ts;
408      searchResults.utids[i] = -1; // We don't know anything about utids.
409      searchResults.sources.push('event');
410      searchResults.trackUris.push(iter.uri);
411      ++i;
412    }
413
414    if (generation !== this._searchGeneration) {
415      // We arrived too late. By the time we computed results the user issued
416      // another search.
417      return;
418    }
419    this._results = searchResults;
420
421    // We have changed the search results - try and find the first result that's
422    // after the start of this visible window.
423    const visibleWindow = this._timeline?.visibleWindow.toTimeSpan();
424    if (visibleWindow) {
425      const foundIndex = this._results.tses.findIndex(
426        (ts) => ts >= visibleWindow.start,
427      );
428      if (foundIndex === -1) {
429        this._resultIndex = -1;
430      } else {
431        // Store the value before the found one, so that when the user presses
432        // enter we navigate to the correct one.
433        this._resultIndex = foundIndex - 1;
434      }
435    } else {
436      this._resultIndex = -1;
437    }
438  }
439}
440