• 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 {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