• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {sqliteString} from '../base/string_utils';
16import {Duration, duration, Span, time, Time, TimeSpan} from '../base/time';
17import {exists} from '../base/utils';
18import {
19  CurrentSearchResults,
20  SearchSource,
21  SearchSummary,
22} from '../common/search_data';
23import {OmniboxState} from '../common/state';
24import {CPU_SLICE_TRACK_KIND} from '../core/track_kinds';
25import {globals} from '../frontend/globals';
26import {publishSearch, publishSearchResult} from '../frontend/publish';
27import {Engine} from '../trace_processor/engine';
28import {LONG, NUM, STR} from '../trace_processor/query_result';
29import {escapeSearchQuery} from '../trace_processor/query_utils';
30
31import {Controller} from './controller';
32
33export interface SearchControllerArgs {
34  engine: Engine;
35}
36
37export class SearchController extends Controller<'main'> {
38  private engine: Engine;
39  private previousSpan: Span<time, duration>;
40  private previousResolution: duration;
41  private previousOmniboxState?: OmniboxState;
42  private updateInProgress: boolean;
43  private setupInProgress: boolean;
44
45  constructor(args: SearchControllerArgs) {
46    super('main');
47    this.engine = args.engine;
48    this.previousSpan = new TimeSpan(Time.fromRaw(0n), Time.fromRaw(1n));
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 (
75      visibleState === undefined ||
76      omniboxState === undefined ||
77      omniboxState.mode === 'COMMAND'
78    ) {
79      return;
80    }
81    const newSpan = globals.stateVisibleTime();
82    const newOmniboxState = omniboxState;
83    const newResolution = visibleState.resolution;
84    if (
85      this.previousSpan.contains(newSpan) &&
86      this.previousResolution === newResolution &&
87      this.previousOmniboxState === newOmniboxState
88    ) {
89      return;
90    }
91
92    // TODO(hjd): We should restrict this to the start of the trace but
93    // that is not easily available here.
94    // N.B. Timestamps can be negative.
95    const {start, end} = newSpan.pad(newSpan.duration);
96    this.previousSpan = new TimeSpan(start, end);
97    this.previousResolution = newResolution;
98    this.previousOmniboxState = newOmniboxState;
99    const search = newOmniboxState.omnibox;
100    if (search === '' || (search.length < 4 && !newOmniboxState.force)) {
101      publishSearch({
102        tsStarts: new BigInt64Array(0),
103        tsEnds: new BigInt64Array(0),
104        count: new Uint8Array(0),
105      });
106      publishSearchResult({
107        eventIds: new Float64Array(0),
108        tses: new BigInt64Array(0),
109        utids: new Float64Array(0),
110        sources: [],
111        trackKeys: [],
112        totalResults: 0,
113      });
114      return;
115    }
116
117    this.updateInProgress = true;
118    const computeSummary = this.update(
119      search,
120      newSpan.start,
121      newSpan.end,
122      newResolution,
123    ).then((summary) => {
124      publishSearch(summary);
125    });
126
127    const computeResults = this.specificSearch(search).then((searchResults) => {
128      publishSearchResult(searchResults);
129    });
130
131    Promise.all([computeSummary, computeResults]).finally(() => {
132      this.updateInProgress = false;
133      this.run();
134    });
135  }
136
137  onDestroy() {}
138
139  private async update(
140    search: string,
141    start: time,
142    end: time,
143    resolution: duration,
144  ): Promise<SearchSummary> {
145    const searchLiteral = escapeSearchQuery(search);
146
147    const quantum = resolution * 10n;
148    start = Time.quantFloor(start, quantum);
149
150    const windowDur = Duration.max(Time.diff(end, start), 1n);
151    await this.query(`update search_summary_window set
152      window_start=${start},
153      window_dur=${windowDur},
154      quantum=${quantum}
155      where rowid = 0;`);
156
157    const utidRes = await this.query(`select utid from thread join process
158      using(upid) where thread.name glob ${searchLiteral}
159      or process.name glob ${searchLiteral}`);
160
161    const utids = [];
162    for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
163      utids.push(it.utid);
164    }
165
166    const cpus = globals.traceContext.cpus;
167    const maxCpu = Math.max(...cpus, -1);
168
169    const res = await this.query(`
170        select
171          (quantum_ts * ${quantum} + ${start}) as tsStart,
172          ((quantum_ts+1) * ${quantum} + ${start}) 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 glob ${searchLiteral}
184          )
185          group by quantum_ts
186          order by quantum_ts;`);
187
188    const numRows = res.numRows();
189    const summary: SearchSummary = {
190      tsStarts: new BigInt64Array(numRows),
191      tsEnds: new BigInt64Array(numRows),
192      count: new Uint8Array(numRows),
193    };
194
195    const it = res.iter({tsStart: LONG, tsEnd: LONG, count: NUM});
196    for (let row = 0; it.valid(); it.next(), ++row) {
197      summary.tsStarts[row] = it.tsStart;
198      summary.tsEnds[row] = it.tsEnd;
199      summary.count[row] = it.count;
200    }
201    return summary;
202  }
203
204  private async specificSearch(search: string) {
205    const searchLiteral = escapeSearchQuery(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    for (const track of Object.values(globals.state.tracks)) {
210      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
211      if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
212        exists(trackInfo.cpu) && cpuToTrackId.set(trackInfo.cpu, track.key);
213      }
214    }
215
216    const utidRes = await this.query(`select utid from thread join process
217    using(upid) where
218      thread.name glob ${searchLiteral} or
219      process.name glob ${searchLiteral}`);
220    const utids = [];
221    for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
222      utids.push(it.utid);
223    }
224
225    const res = await this.query(`
226      select
227        id as sliceId,
228        ts,
229        'cpu' as source,
230        cpu as sourceId,
231        utid
232      from sched where utid in (${utids.join(',')})
233      union all
234      select *
235      from (
236        select
237          slice_id as sliceId,
238          ts,
239          'slice' as source,
240          track_id as sourceId,
241          0 as utid
242          from slice
243          where slice.name glob ${searchLiteral}
244            or (
245              0 != CAST(${sqliteString(search)} AS INT) and
246              sliceId = CAST(${sqliteString(search)} AS INT)
247            )
248        union
249        select
250          slice_id as sliceId,
251          ts,
252          'slice' as source,
253          track_id as sourceId,
254          0 as utid
255        from slice
256        join args using(arg_set_id)
257        where string_value glob ${searchLiteral} or key glob ${searchLiteral}
258      )
259      union all
260      select
261        id as sliceId,
262        ts,
263        'log' as source,
264        0 as sourceId,
265        utid
266      from android_logs where msg glob ${searchLiteral}
267      order by ts
268    `);
269
270    const searchResults: CurrentSearchResults = {
271      eventIds: new Float64Array(0),
272      tses: new BigInt64Array(0),
273      utids: new Float64Array(0),
274      sources: [],
275      trackKeys: [],
276      totalResults: 0,
277    };
278
279    const lowerSearch = search.toLowerCase();
280    for (const track of Object.values(globals.state.tracks)) {
281      if (track.name.toLowerCase().indexOf(lowerSearch) === -1) {
282        continue;
283      }
284      searchResults.totalResults++;
285      searchResults.sources.push('track');
286      searchResults.trackKeys.push(track.key);
287    }
288
289    const rows = res.numRows();
290    searchResults.eventIds = new Float64Array(
291      searchResults.totalResults + rows,
292    );
293    searchResults.tses = new BigInt64Array(searchResults.totalResults + rows);
294    searchResults.utids = new Float64Array(searchResults.totalResults + rows);
295    for (let i = 0; i < searchResults.totalResults; ++i) {
296      searchResults.eventIds[i] = -1;
297      searchResults.tses[i] = -1n;
298      searchResults.utids[i] = -1;
299    }
300
301    const it = res.iter({
302      sliceId: NUM,
303      ts: LONG,
304      source: STR,
305      sourceId: NUM,
306      utid: NUM,
307    });
308    for (; it.valid(); it.next()) {
309      let trackId = undefined;
310      if (it.source === 'cpu') {
311        trackId = cpuToTrackId.get(it.sourceId);
312      } else if (it.source === 'slice') {
313        trackId = globals.trackManager.trackKeyByTrackId.get(it.sourceId);
314      } else if (it.source === 'log') {
315        const logTracks = Object.values(globals.state.tracks).filter(
316          (track) => {
317            const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
318            return trackDesc && trackDesc.kind === 'AndroidLogTrack';
319          },
320        );
321        if (logTracks.length > 0) {
322          trackId = logTracks[0].key;
323        }
324      }
325
326      // The .get() calls above could return undefined, this isn't just an else.
327      if (trackId === undefined) {
328        continue;
329      }
330
331      const i = searchResults.totalResults++;
332      searchResults.trackKeys.push(trackId);
333      searchResults.sources.push(it.source as SearchSource);
334      searchResults.eventIds[i] = it.sliceId;
335      searchResults.tses[i] = it.ts;
336      searchResults.utids[i] = it.utid;
337    }
338    return searchResults;
339  }
340
341  private async query(query: string) {
342    const result = await this.engine.query(query);
343    return result;
344  }
345}
346