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