// Copyright (C) 2019 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import {Engine} from '../common/engine'; import { LogBounds, LogBoundsKey, LogEntries, LogEntriesKey, LogExistsKey } from '../common/logs'; import {NUM, STR} from '../common/query_result'; import {fromNs, TimeSpan, toNsCeil, toNsFloor} from '../common/time'; import {publishTrackData} from '../frontend/publish'; import {Controller} from './controller'; import {App} from './globals'; async function updateLogBounds( engine: Engine, span: TimeSpan): Promise { const vizStartNs = toNsFloor(span.start); const vizEndNs = toNsCeil(span.end); const countResult = await engine.query(` select ifnull(min(ts), 0) as minTs, ifnull(max(ts), 0) as maxTs, count(ts) as countTs from android_logs where ts >= ${vizStartNs} and ts <= ${vizEndNs}`); const countRow = countResult.firstRow({minTs: NUM, maxTs: NUM, countTs: NUM}); const firstRowNs = countRow.minTs; const lastRowNs = countRow.maxTs; const total = countRow.countTs; const minResult = await engine.query(` select ifnull(max(ts), 0) as maxTs from android_logs where ts < ${ vizStartNs}`); const startNs = minResult.firstRow({maxTs: NUM}).maxTs; const maxResult = await engine.query(` select ifnull(min(ts), 0) as minTs from android_logs where ts > ${ vizEndNs}`); const endNs = maxResult.firstRow({minTs: NUM}).minTs; const trace = await engine.getTraceTimeBounds(); const startTs = startNs ? fromNs(startNs) : trace.start; const endTs = endNs ? fromNs(endNs) : trace.end; const firstRowTs = firstRowNs ? fromNs(firstRowNs) : endTs; const lastRowTs = lastRowNs ? fromNs(lastRowNs) : startTs; return { startTs, endTs, firstRowTs, lastRowTs, total, }; } async function updateLogEntries( engine: Engine, span: TimeSpan, pagination: Pagination): Promise { const vizStartNs = toNsFloor(span.start); const vizEndNs = toNsCeil(span.end); const vizSqlBounds = `ts >= ${vizStartNs} and ts <= ${vizEndNs}`; const rowsResult = await engine.query(` select ts, prio, ifnull(tag, '[NULL]') as tag, ifnull(msg, '[NULL]') as msg from android_logs where ${vizSqlBounds} order by ts limit ${pagination.start}, ${pagination.count} `); const timestamps = []; const priorities = []; const tags = []; const messages = []; const it = rowsResult.iter({ts: NUM, prio: NUM, tag: STR, msg: STR}); for (; it.valid(); it.next()) { timestamps.push(it.ts); priorities.push(it.prio); tags.push(it.tag); messages.push(it.msg); } return { offset: pagination.start, timestamps, priorities, tags, messages, }; } class Pagination { private _offset: number; private _count: number; constructor(offset: number, count: number) { this._offset = offset; this._count = count; } get start() { return this._offset; } get count() { return this._count; } get end() { return this._offset + this._count; } contains(other: Pagination): boolean { return this.start <= other.start && other.end <= this.end; } grow(n: number): Pagination { const newStart = Math.max(0, this.start - n / 2); const newCount = this.count + n; return new Pagination(newStart, newCount); } } export interface LogsControllerArgs { engine: Engine; app: App; } /** * LogsController looks at two parts of the state: * 1. The visible trace window * 2. The requested offset and count the log lines to display * And keeps two bits of published information up to date: * 1. The total number of log messages in visible range * 2. The logs lines that should be displayed */ export class LogsController extends Controller<'main'> { private app: App; private engine: Engine; private span: TimeSpan; private pagination: Pagination; private hasLogs = false; constructor(args: LogsControllerArgs) { super('main'); this.app = args.app; this.engine = args.engine; this.span = new TimeSpan(0, 10); this.pagination = new Pagination(0, 0); this.hasAnyLogs().then(exists => { this.hasLogs = exists; publishTrackData({ id: LogExistsKey, data: { exists, }, }); }); } async hasAnyLogs() { const result = await this.engine.query(` select count(*) as cnt from android_logs `); return result.firstRow({cnt: NUM}).cnt > 0; } run() { if (!this.hasLogs) return; const traceTime = this.app.state.frontendLocalState.visibleState; const newSpan = new TimeSpan(traceTime.startSec, traceTime.endSec); const oldSpan = this.span; const pagination = this.app.state.logsPagination; // This can occur when loading old traces. // TODO(hjd): Fix the problem of accessing state from a previous version of // the UI in a general way. if (pagination === undefined) { return; } const {offset, count} = pagination; const requestedPagination = new Pagination(offset, count); const oldPagination = this.pagination; const needSpanUpdate = !oldSpan.equals(newSpan); const needPaginationUpdate = !oldPagination.contains(requestedPagination); // TODO(hjd): We could waste a lot of time queueing useless updates here. // We should avoid enqueuing a request when one is in progress. if (needSpanUpdate) { this.span = newSpan; updateLogBounds(this.engine, newSpan).then(data => { if (!newSpan.equals(this.span)) return; publishTrackData({ id: LogBoundsKey, data, }); }); } // TODO(hjd): We could waste a lot of time queueing useless updates here. // We should avoid enqueuing a request when one is in progress. if (needSpanUpdate || needPaginationUpdate) { this.pagination = requestedPagination.grow(100); updateLogEntries(this.engine, newSpan, this.pagination).then(data => { if (!this.pagination.contains(requestedPagination)) return; publishTrackData({ id: LogEntriesKey, data, }); }); } return []; } }