• 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 {Engine} from '../common/engine';
16import {
17  LogBounds,
18  LogBoundsKey,
19  LogEntries,
20  LogEntriesKey,
21  LogExistsKey
22} from '../common/logs';
23import {fromNs, TimeSpan} from '../common/time';
24
25import {Controller} from './controller';
26import {App} from './globals';
27
28async function updateLogBounds(
29    engine: Engine, span: TimeSpan): Promise<LogBounds> {
30  const vizStartNs = Math.floor(span.start * 1e9);
31  const vizEndNs = Math.ceil(span.end * 1e9);
32
33  const countResult = await engine.queryOneRow(`
34     select min(ts), max(ts), count(ts)
35     from android_logs where ts >= ${vizStartNs} and ts <= ${vizEndNs}`);
36
37  const firstRowNs = countResult[0];
38  const lastRowNs = countResult[1];
39  const total = countResult[2];
40
41  const minResult = await engine.queryOneRow(`
42     select max(ts) from android_logs where ts < ${vizStartNs}`);
43  const startNs = minResult[0];
44
45  const maxResult = await engine.queryOneRow(`
46     select min(ts) from android_logs where ts > ${vizEndNs}`);
47  const endNs = maxResult[0];
48
49  const trace = await engine.getTraceTimeBounds();
50  const startTs = startNs ? fromNs(startNs) : trace.start;
51  const endTs = endNs ? fromNs(endNs) : trace.end;
52  const firstRowTs = firstRowNs ? fromNs(firstRowNs) : endTs;
53  const lastRowTs = lastRowNs ? fromNs(lastRowNs) : startTs;
54  return {
55    startTs,
56    endTs,
57    firstRowTs,
58    lastRowTs,
59    total,
60  };
61}
62
63async function updateLogEntries(
64    engine: Engine, span: TimeSpan, pagination: Pagination):
65    Promise<LogEntries> {
66  const vizStartNs = Math.floor(span.start * 1e9);
67  const vizEndNs = Math.ceil(span.end * 1e9);
68  const vizSqlBounds = `ts >= ${vizStartNs} and ts <= ${vizEndNs}`;
69
70  const rowsResult =
71      await engine.query(`select ts, prio, tag, msg from android_logs
72        where ${vizSqlBounds}
73        order by ts
74        limit ${pagination.start}, ${pagination.count}`);
75
76  if (!rowsResult.numRecords) {
77    return {
78      offset: pagination.start,
79      timestamps: [],
80      priorities: [],
81      tags: [],
82      messages: [],
83    };
84  }
85
86  const timestamps = rowsResult.columns[0].longValues! as number[];
87  const priorities = rowsResult.columns[1].longValues! as number[];
88  const tags = rowsResult.columns[2].stringValues!;
89  const messages = rowsResult.columns[3].stringValues!;
90
91  return {
92    offset: pagination.start,
93    timestamps,
94    priorities,
95    tags,
96    messages,
97  };
98}
99
100class Pagination {
101  private _offset: number;
102  private _count: number;
103
104  constructor(offset: number, count: number) {
105    this._offset = offset;
106    this._count = count;
107  }
108
109  get start() {
110    return this._offset;
111  }
112
113  get count() {
114    return this._count;
115  }
116
117  get end() {
118    return this._offset + this._count;
119  }
120
121  contains(other: Pagination): boolean {
122    return this.start <= other.start && other.end <= this.end;
123  }
124
125  grow(n: number): Pagination {
126    const newStart = Math.max(0, this.start - n / 2);
127    const newCount = this.count + n;
128    return new Pagination(newStart, newCount);
129  }
130}
131
132export interface LogsControllerArgs {
133  engine: Engine;
134  app: App;
135}
136
137/**
138 * LogsController looks at two parts of the state:
139 * 1. The visible trace window
140 * 2. The requested offset and count the log lines to display
141 * And keeps two bits of published information up to date:
142 * 1. The total number of log messages in visible range
143 * 2. The logs lines that should be displayed
144 */
145export class LogsController extends Controller<'main'> {
146  private app: App;
147  private engine: Engine;
148  private span: TimeSpan;
149  private pagination: Pagination;
150
151  constructor(args: LogsControllerArgs) {
152    super('main');
153    this.app = args.app;
154    this.engine = args.engine;
155    this.span = new TimeSpan(0, 10);
156    this.pagination = new Pagination(0, 0);
157    this.publishHasAnyLogs();
158  }
159
160  async publishHasAnyLogs() {
161    const result = await this.engine.queryOneRow(`
162      select count(*) from android_logs
163    `);
164    const exists = result[0] > 0;
165    this.app.publish('TrackData', {
166      id: LogExistsKey,
167      data: {
168        exists,
169      },
170    });
171  }
172
173  run() {
174    const traceTime = this.app.state.frontendLocalState.visibleTraceTime;
175    const newSpan = new TimeSpan(traceTime.startSec, traceTime.endSec);
176    const oldSpan = this.span;
177
178    const pagination = this.app.state.logsPagination;
179    // This can occur when loading old traces.
180    // TODO(hjd): Fix the problem of accessing state from a previous version of
181    // the UI in a general way.
182    if (pagination === undefined) {
183      return;
184    }
185
186    const {offset, count} = pagination;
187    const requestedPagination = new Pagination(offset, count);
188    const oldPagination = this.pagination;
189
190    const needSpanUpdate = !oldSpan.equals(newSpan);
191    const needPaginationUpdate = !oldPagination.contains(requestedPagination);
192
193    // TODO(hjd): We could waste a lot of time queueing useless updates here.
194    // We should avoid enqueuing a request when one is in progress.
195    if (needSpanUpdate) {
196      this.span = newSpan;
197      updateLogBounds(this.engine, newSpan).then(data => {
198        if (!newSpan.equals(this.span)) return;
199        this.app.publish('TrackData', {
200          id: LogBoundsKey,
201          data,
202        });
203      });
204    }
205
206    // TODO(hjd): We could waste a lot of time queueing useless updates here.
207    // We should avoid enqueuing a request when one is in progress.
208    if (needSpanUpdate || needPaginationUpdate) {
209      this.pagination = requestedPagination.grow(100);
210
211      updateLogEntries(this.engine, newSpan, this.pagination).then(data => {
212        if (!this.pagination.contains(requestedPagination)) return;
213        this.app.publish('TrackData', {
214          id: LogEntriesKey,
215          data,
216        });
217      });
218    }
219
220    return [];
221  }
222}
223