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