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