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