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 {TRACE_MARGIN_TIME_S} from '../common/constants';
16import {Engine} from '../common/engine';
17import {slowlyCountRows} from '../common/query_iterator';
18import {CurrentSearchResults, SearchSummary} from '../common/search_data';
19import {TimeSpan} from '../common/time';
20
21import {Controller} from './controller';
22import {App} from './globals';
23
24export function escapeQuery(s: string): string {
25  // See https://www.sqlite.org/lang_expr.html#:~:text=A%20string%20constant
26  s = s.replace(/\'/g, '\'\'');
27  s = s.replace(/_/g, '^_');
28  s = s.replace(/%/g, '^%');
29  return `'%${s}%' escape '^'`;
30}
31
32export interface SearchControllerArgs {
33  engine: Engine;
34  app: App;
35}
36
37export class SearchController extends Controller<'main'> {
38  private engine: Engine;
39  private app: App;
40  private previousSpan: TimeSpan;
41  private previousResolution: number;
42  private previousSearch: string;
43  private updateInProgress: boolean;
44  private setupInProgress: boolean;
45
46  constructor(args: SearchControllerArgs) {
47    super('main');
48    this.engine = args.engine;
49    this.app = args.app;
50    this.previousSpan = new TimeSpan(0, 1);
51    this.previousSearch = '';
52    this.updateInProgress = false;
53    this.setupInProgress = true;
54    this.previousResolution = 1;
55    this.setup().finally(() => {
56      this.setupInProgress = false;
57      this.run();
58    });
59  }
60
61  private async setup() {
62    await this.query(`create virtual table search_summary_window
63      using window;`);
64    await this.query(`create virtual table search_summary_sched_span using
65      span_join(sched PARTITIONED cpu, search_summary_window);`);
66    await this.query(`create virtual table search_summary_slice_span using
67      span_join(slice PARTITIONED track_id, search_summary_window);`);
68  }
69
70  run() {
71    if (this.setupInProgress || this.updateInProgress) {
72      return;
73    }
74
75    const visibleState = this.app.state.frontendLocalState.visibleState;
76    const omniboxState = this.app.state.frontendLocalState.omniboxState;
77    if (visibleState === undefined || omniboxState === undefined ||
78        omniboxState.mode === 'COMMAND') {
79      return;
80    }
81    const newSpan = new TimeSpan(visibleState.startSec, visibleState.endSec);
82    const newSearch = omniboxState.omnibox;
83    let newResolution = visibleState.resolution;
84    if (this.previousSpan.contains(newSpan) &&
85        this.previousResolution === newResolution &&
86        newSearch === this.previousSearch) {
87      return;
88    }
89    this.previousSpan = new TimeSpan(
90        Math.max(newSpan.start - newSpan.duration, -TRACE_MARGIN_TIME_S),
91        newSpan.end + newSpan.duration);
92    this.previousResolution = newResolution;
93    this.previousSearch = newSearch;
94    if (newSearch === '' || newSearch.length < 4) {
95      this.app.publish('Search', {
96        tsStarts: new Float64Array(0),
97        tsEnds: new Float64Array(0),
98        count: new Uint8Array(0),
99      });
100      this.app.publish('SearchResult', {
101        sliceIds: new Float64Array(0),
102        tsStarts: new Float64Array(0),
103        utids: new Float64Array(0),
104        sources: [],
105        trackIds: [],
106        totalResults: 0,
107      });
108      return;
109    }
110
111    let startNs = Math.round(newSpan.start * 1e9);
112    let endNs = Math.round(newSpan.end * 1e9);
113
114    // TODO(hjd): We shouldn't need to be so defensive here:
115    if (!Number.isFinite(startNs)) {
116      startNs = 0;
117    }
118    if (!Number.isFinite(endNs)) {
119      endNs = 1;
120    }
121    if (!Number.isFinite(newResolution)) {
122      newResolution = 1;
123    }
124
125    this.updateInProgress = true;
126    const computeSummary =
127        this.update(newSearch, startNs, endNs, newResolution).then(summary => {
128          this.app.publish('Search', summary);
129        });
130
131    const computeResults =
132        this.specificSearch(newSearch).then(searchResults => {
133          this.app.publish('SearchResult', searchResults);
134        });
135
136    Promise.all([computeSummary, computeResults])
137        .finally(() => {
138          this.updateInProgress = false;
139          this.run();
140        });
141  }
142
143  onDestroy() {}
144
145  private async update(
146      search: string, startNs: number, endNs: number,
147      resolution: number): Promise<SearchSummary> {
148    const quantumNs = Math.round(resolution * 10 * 1e9);
149
150    const searchLiteral = escapeQuery(search);
151
152    startNs = Math.floor(startNs / quantumNs) * quantumNs;
153
154    await this.query(`update search_summary_window set
155      window_start=${startNs},
156      window_dur=${endNs - startNs},
157      quantum=${quantumNs}
158      where rowid = 0;`);
159
160    const rawUtidResult = await this.query(`select utid from thread join process
161      using(upid) where thread.name like ${searchLiteral}
162      or process.name like ${searchLiteral}`);
163
164    const utids = [...rawUtidResult.columns[0].longValues!];
165
166    const cpus = await this.engine.getCpus();
167    const maxCpu = Math.max(...cpus, -1);
168
169    const rawResult = await this.query(`
170        select
171          (quantum_ts * ${quantumNs} + ${startNs})/1e9 as tsStart,
172          ((quantum_ts+1) * ${quantumNs} + ${startNs})/1e9 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 like ${searchLiteral}
184          )
185          group by quantum_ts
186          order by quantum_ts;`);
187
188    const numRows = slowlyCountRows(rawResult);
189    const summary = {
190      tsStarts: new Float64Array(numRows),
191      tsEnds: new Float64Array(numRows),
192      count: new Uint8Array(numRows)
193    };
194
195    const columns = rawResult.columns;
196    for (let row = 0; row < numRows; row++) {
197      summary.tsStarts[row] = +columns[0].doubleValues![row];
198      summary.tsEnds[row] = +columns[1].doubleValues![row];
199      summary.count[row] = +columns[2].longValues![row];
200    }
201    return summary;
202  }
203
204  private async specificSearch(search: string) {
205    const searchLiteral = escapeQuery(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    const engineTrackIdToTrackId = new Map();
210    for (const track of Object.values(this.app.state.tracks)) {
211      if (track.kind === 'CpuSliceTrack') {
212        cpuToTrackId.set((track.config as {cpu: number}).cpu, track.id);
213        continue;
214      }
215      if (track.kind === 'ChromeSliceTrack') {
216        const config = (track.config as {trackId: number});
217        engineTrackIdToTrackId.set(config.trackId, track.id);
218        continue;
219      }
220      if (track.kind === 'AsyncSliceTrack') {
221        const config = (track.config as {trackIds: number[]});
222        for (const trackId of config.trackIds) {
223          engineTrackIdToTrackId.set(trackId, track.id);
224        }
225        continue;
226      }
227    }
228
229    const rawUtidResult = await this.query(`select utid from thread join process
230    using(upid) where
231      thread.name like ${searchLiteral} or
232      process.name like ${searchLiteral}`);
233    const utids = [...rawUtidResult.columns[0].longValues!];
234
235    const rawResult = await this.query(`
236    select
237      id as slice_id,
238      ts,
239      'cpu' as source,
240      cpu as source_id,
241      utid
242    from sched where utid in (${utids.join(',')})
243    union
244    select
245      slice_id,
246      ts,
247      'track' as source,
248      track_id as source_id,
249      0 as utid
250      from slice
251      where slice.name like ${searchLiteral}
252    union
253    select
254      slice_id,
255      ts,
256      'track' as source,
257      track_id as source_id,
258      0 as utid
259      from slice
260      join args using(arg_set_id)
261      where string_value like ${searchLiteral}
262    order by ts`);
263
264    const numRows = slowlyCountRows(rawResult);
265
266    const searchResults: CurrentSearchResults = {
267      sliceIds: [],
268      tsStarts: [],
269      utids: [],
270      trackIds: [],
271      sources: [],
272      totalResults: +numRows,
273    };
274
275    const columns = rawResult.columns;
276    for (let row = 0; row < numRows; row++) {
277      const source = columns[2].stringValues![row];
278      const sourceId = +columns[3].longValues![row];
279      let trackId = undefined;
280      if (source === 'cpu') {
281        trackId = cpuToTrackId.get(sourceId);
282      } else if (source === 'track') {
283        trackId = engineTrackIdToTrackId.get(sourceId);
284      }
285
286      if (trackId === undefined) {
287        searchResults.totalResults--;
288        continue;
289      }
290
291      searchResults.trackIds.push(trackId);
292      searchResults.sources.push(source);
293      searchResults.sliceIds.push(+columns[0].longValues![row]);
294      searchResults.tsStarts.push(+columns[1].longValues![row]);
295      searchResults.utids.push(+columns[4].longValues![row]);
296    }
297    return searchResults;
298  }
299
300
301  private async query(query: string) {
302    const result = await this.engine.query(query);
303    return result;
304  }
305}
306