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