• 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 {Engine} from '../common/engine';
17import {
18  LogBounds,
19  LogBoundsKey,
20  LogEntries,
21  LogEntriesKey,
22  LogExistsKey,
23} from '../common/logs';
24import {LONG, LONG_NULL, NUM, STR} from '../common/query_result';
25import {escapeGlob, escapeQuery} from '../common/query_utils';
26import {LogFilteringCriteria} from '../common/state';
27import {Span} from '../common/time';
28import {
29  TPTime,
30  TPTimeSpan,
31} from '../common/time';
32import {globals} from '../frontend/globals';
33import {publishTrackData} from '../frontend/publish';
34
35import {Controller} from './controller';
36
37async function updateLogBounds(
38    engine: Engine, span: Span<TPTime>): Promise<LogBounds> {
39  const vizStartNs = span.start;
40  const vizEndNs = span.end;
41
42  const vizFilter = `ts between ${vizStartNs} and ${vizEndNs}`;
43
44  const result = await engine.query(`select
45      min(ts) as minTs,
46      max(ts) as maxTs,
47      min(case when ${vizFilter} then ts end) as minVizTs,
48      max(case when ${vizFilter} then ts end) as maxVizTs,
49      count(case when ${vizFilter} then ts end) as countTs
50    from filtered_logs`);
51
52  const data = result.firstRow({
53    minTs: LONG_NULL,
54    maxTs: LONG_NULL,
55    minVizTs: LONG_NULL,
56    maxVizTs: LONG_NULL,
57    countTs: NUM,
58  });
59
60  const firstLogTs = data.minTs ?? 0n;
61  const lastLogTs = data.maxTs ?? BigintMath.INT64_MAX;
62
63  const bounds: LogBounds = {
64    firstLogTs,
65    lastLogTs,
66    firstVisibleLogTs: data.minVizTs ?? firstLogTs,
67    lastVisibleLogTs: data.maxVizTs ?? lastLogTs,
68    totalVisibleLogs: data.countTs,
69  };
70
71  return bounds;
72}
73
74async function updateLogEntries(
75    engine: Engine, span: Span<TPTime>, pagination: Pagination):
76    Promise<LogEntries> {
77  const vizStartNs = span.start;
78  const vizEndNs = span.end;
79  const vizSqlBounds = `ts >= ${vizStartNs} and ts <= ${vizEndNs}`;
80
81  const rowsResult = await engine.query(`
82        select
83          ts,
84          prio,
85          ifnull(tag, '[NULL]') as tag,
86          ifnull(msg, '[NULL]') as msg,
87          is_msg_highlighted as isMsgHighlighted,
88          is_process_highlighted as isProcessHighlighted,
89          ifnull(process_name, '') as processName
90        from filtered_logs
91        where ${vizSqlBounds}
92        order by ts
93        limit ${pagination.start}, ${pagination.count}
94    `);
95
96  const timestamps = [];
97  const priorities = [];
98  const tags = [];
99  const messages = [];
100  const isHighlighted = [];
101  const processName = [];
102
103  const it = rowsResult.iter({
104    ts: LONG,
105    prio: NUM,
106    tag: STR,
107    msg: STR,
108    isMsgHighlighted: NUM,
109    isProcessHighlighted: NUM,
110    processName: STR,
111  });
112  for (; it.valid(); it.next()) {
113    timestamps.push(it.ts);
114    priorities.push(it.prio);
115    tags.push(it.tag);
116    messages.push(it.msg);
117    isHighlighted.push(
118        it.isMsgHighlighted === 1 || it.isProcessHighlighted === 1);
119    processName.push(it.processName);
120  }
121
122  return {
123    offset: pagination.start,
124    timestamps,
125    priorities,
126    tags,
127    messages,
128    isHighlighted,
129    processName,
130  };
131}
132
133class Pagination {
134  private _offset: number;
135  private _count: number;
136
137  constructor(offset: number, count: number) {
138    this._offset = offset;
139    this._count = count;
140  }
141
142  get start() {
143    return this._offset;
144  }
145
146  get count() {
147    return this._count;
148  }
149
150  get end() {
151    return this._offset + this._count;
152  }
153
154  contains(other: Pagination): boolean {
155    return this.start <= other.start && other.end <= this.end;
156  }
157
158  grow(n: number): Pagination {
159    const newStart = Math.max(0, this.start - n / 2);
160    const newCount = this.count + n;
161    return new Pagination(newStart, newCount);
162  }
163}
164
165export interface LogsControllerArgs {
166  engine: Engine;
167}
168
169/**
170 * LogsController looks at three parts of the state:
171 * 1. The visible trace window
172 * 2. The requested offset and count the log lines to display
173 * 3. The log filtering criteria.
174 * And keeps two bits of published information up to date:
175 * 1. The total number of log messages in visible range
176 * 2. The logs lines that should be displayed
177 * Based on the log filtering criteria, it also builds the filtered_logs view
178 * and keeps it up to date.
179 */
180export class LogsController extends Controller<'main'> {
181  private engine: Engine;
182  private span: Span<TPTime>;
183  private pagination: Pagination;
184  private hasLogs = false;
185  private logFilteringCriteria?: LogFilteringCriteria;
186  private requestingData = false;
187  private queuedRunRequest = false;
188
189  constructor(args: LogsControllerArgs) {
190    super('main');
191    this.engine = args.engine;
192    this.span = new TPTimeSpan(0n, BigInt(10e9));
193    this.pagination = new Pagination(0, 0);
194    this.hasAnyLogs().then((exists) => {
195      this.hasLogs = exists;
196      publishTrackData({
197        id: LogExistsKey,
198        data: {
199          exists,
200        },
201      });
202    });
203  }
204
205  async hasAnyLogs() {
206    const result = await this.engine.query(`
207      select count(*) as cnt from android_logs
208    `);
209    return result.firstRow({cnt: NUM}).cnt > 0;
210  }
211
212  run() {
213    if (!this.hasLogs) return;
214    if (this.requestingData) {
215      this.queuedRunRequest = true;
216      return;
217    }
218    this.requestingData = true;
219    this.updateLogTracks().finally(() => {
220      this.requestingData = false;
221      if (this.queuedRunRequest) {
222        this.queuedRunRequest = false;
223        this.run();
224      }
225    });
226  }
227
228  private async updateLogTracks() {
229    const newSpan = globals.stateVisibleTime();
230    const oldSpan = this.span;
231
232    const pagination = globals.state.logsPagination;
233    // This can occur when loading old traces.
234    // TODO(hjd): Fix the problem of accessing state from a previous version of
235    // the UI in a general way.
236    if (pagination === undefined) {
237      return;
238    }
239
240    const {offset, count} = pagination;
241    const requestedPagination = new Pagination(offset, count);
242    const oldPagination = this.pagination;
243
244    const newFilteringCriteria =
245        this.logFilteringCriteria !== globals.state.logFilteringCriteria;
246    const needBoundsUpdate = !oldSpan.equals(newSpan) || newFilteringCriteria;
247    const needEntriesUpdate =
248        !oldPagination.contains(requestedPagination) || needBoundsUpdate;
249
250    if (newFilteringCriteria) {
251      this.logFilteringCriteria = globals.state.logFilteringCriteria;
252      await this.engine.query('drop view if exists filtered_logs');
253
254      const globMatch = LogsController.composeGlobMatch(
255          this.logFilteringCriteria.hideNonMatching,
256          this.logFilteringCriteria.textEntry);
257      let selectedRows = `select prio, ts, tag, msg,
258          process.name as process_name, ${globMatch}
259          from android_logs
260          left join thread using(utid)
261          left join process using(upid)
262          where prio >= ${this.logFilteringCriteria.minimumLevel}`;
263      if (this.logFilteringCriteria.tags.length) {
264        selectedRows += ` and tag in (${
265            LogsController.serializeTags(this.logFilteringCriteria.tags)})`;
266      }
267
268      // We extract only the rows which will be visible.
269      await this.engine.query(`create view filtered_logs as select *
270        from (${selectedRows})
271        where is_msg_chosen is 1 or is_process_chosen is 1`);
272    }
273
274    if (needBoundsUpdate) {
275      this.span = newSpan;
276      const logBounds = await updateLogBounds(this.engine, newSpan);
277      publishTrackData({
278        id: LogBoundsKey,
279        data: logBounds,
280      });
281    }
282
283    if (needEntriesUpdate) {
284      this.pagination = requestedPagination.grow(100);
285      const logEntries =
286          await updateLogEntries(this.engine, newSpan, this.pagination);
287      publishTrackData({
288        id: LogEntriesKey,
289        data: logEntries,
290      });
291    }
292  }
293
294  private static serializeTags(tags: string[]) {
295    return tags.map((tag) => escapeQuery(tag)).join();
296  }
297
298  private static composeGlobMatch(isCollaped: boolean, textEntry: string) {
299    if (isCollaped) {
300      // If the entries are collapsed, we won't highlight any lines.
301      return `msg glob ${escapeGlob(textEntry)} as is_msg_chosen,
302        (process.name is not null and process.name glob ${
303          escapeGlob(textEntry)}) as is_process_chosen,
304        0 as is_msg_highlighted,
305        0 as is_process_highlighted`;
306    } else if (!textEntry) {
307      // If there is no text entry, we will show all lines, but won't highlight.
308      // any.
309      return `1 as is_msg_chosen,
310        1 as is_process_chosen,
311        0 as is_msg_highlighted,
312        0 as is_process_highlighted`;
313    } else {
314      return `1 as is_msg_chosen,
315        1 as is_process_chosen,
316        msg glob ${escapeGlob(textEntry)} as is_msg_highlighted,
317        (process.name is not null and process.name glob ${
318          escapeGlob(textEntry)}) as is_process_highlighted`;
319    }
320  }
321}
322