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 {BigintMath} from '../base/bigint_math'; 16import {Engine} from '../common/engine'; 17import { 18 LogBounds, 19 LogBoundsKey, 20 LogEntries, 21 LogEntriesKey, 22 LogExistsKey, 23} from '../common/logs'; 24import {LONG, LONG_NULL, NUM, STR} from '../common/query_result'; 25import {escapeGlob, escapeQuery} from '../common/query_utils'; 26import {LogFilteringCriteria} from '../common/state'; 27import {Span} from '../common/time'; 28import { 29 TPTime, 30 TPTimeSpan, 31} from '../common/time'; 32import {globals} from '../frontend/globals'; 33import {publishTrackData} from '../frontend/publish'; 34 35import {Controller} from './controller'; 36 37async function updateLogBounds( 38 engine: Engine, span: Span<TPTime>): Promise<LogBounds> { 39 const vizStartNs = span.start; 40 const vizEndNs = span.end; 41 42 const vizFilter = `ts between ${vizStartNs} and ${vizEndNs}`; 43 44 const result = await engine.query(`select 45 min(ts) as minTs, 46 max(ts) as maxTs, 47 min(case when ${vizFilter} then ts end) as minVizTs, 48 max(case when ${vizFilter} then ts end) as maxVizTs, 49 count(case when ${vizFilter} then ts end) as countTs 50 from filtered_logs`); 51 52 const data = result.firstRow({ 53 minTs: LONG_NULL, 54 maxTs: LONG_NULL, 55 minVizTs: LONG_NULL, 56 maxVizTs: LONG_NULL, 57 countTs: NUM, 58 }); 59 60 const firstLogTs = data.minTs ?? 0n; 61 const lastLogTs = data.maxTs ?? BigintMath.INT64_MAX; 62 63 const bounds: LogBounds = { 64 firstLogTs, 65 lastLogTs, 66 firstVisibleLogTs: data.minVizTs ?? firstLogTs, 67 lastVisibleLogTs: data.maxVizTs ?? lastLogTs, 68 totalVisibleLogs: data.countTs, 69 }; 70 71 return bounds; 72} 73 74async function updateLogEntries( 75 engine: Engine, span: Span<TPTime>, pagination: Pagination): 76 Promise<LogEntries> { 77 const vizStartNs = span.start; 78 const vizEndNs = span.end; 79 const vizSqlBounds = `ts >= ${vizStartNs} and ts <= ${vizEndNs}`; 80 81 const rowsResult = await engine.query(` 82 select 83 ts, 84 prio, 85 ifnull(tag, '[NULL]') as tag, 86 ifnull(msg, '[NULL]') as msg, 87 is_msg_highlighted as isMsgHighlighted, 88 is_process_highlighted as isProcessHighlighted, 89 ifnull(process_name, '') as processName 90 from filtered_logs 91 where ${vizSqlBounds} 92 order by ts 93 limit ${pagination.start}, ${pagination.count} 94 `); 95 96 const timestamps = []; 97 const priorities = []; 98 const tags = []; 99 const messages = []; 100 const isHighlighted = []; 101 const processName = []; 102 103 const it = rowsResult.iter({ 104 ts: LONG, 105 prio: NUM, 106 tag: STR, 107 msg: STR, 108 isMsgHighlighted: NUM, 109 isProcessHighlighted: NUM, 110 processName: STR, 111 }); 112 for (; it.valid(); it.next()) { 113 timestamps.push(it.ts); 114 priorities.push(it.prio); 115 tags.push(it.tag); 116 messages.push(it.msg); 117 isHighlighted.push( 118 it.isMsgHighlighted === 1 || it.isProcessHighlighted === 1); 119 processName.push(it.processName); 120 } 121 122 return { 123 offset: pagination.start, 124 timestamps, 125 priorities, 126 tags, 127 messages, 128 isHighlighted, 129 processName, 130 }; 131} 132 133class Pagination { 134 private _offset: number; 135 private _count: number; 136 137 constructor(offset: number, count: number) { 138 this._offset = offset; 139 this._count = count; 140 } 141 142 get start() { 143 return this._offset; 144 } 145 146 get count() { 147 return this._count; 148 } 149 150 get end() { 151 return this._offset + this._count; 152 } 153 154 contains(other: Pagination): boolean { 155 return this.start <= other.start && other.end <= this.end; 156 } 157 158 grow(n: number): Pagination { 159 const newStart = Math.max(0, this.start - n / 2); 160 const newCount = this.count + n; 161 return new Pagination(newStart, newCount); 162 } 163} 164 165export interface LogsControllerArgs { 166 engine: Engine; 167} 168 169/** 170 * LogsController looks at three parts of the state: 171 * 1. The visible trace window 172 * 2. The requested offset and count the log lines to display 173 * 3. The log filtering criteria. 174 * And keeps two bits of published information up to date: 175 * 1. The total number of log messages in visible range 176 * 2. The logs lines that should be displayed 177 * Based on the log filtering criteria, it also builds the filtered_logs view 178 * and keeps it up to date. 179 */ 180export class LogsController extends Controller<'main'> { 181 private engine: Engine; 182 private span: Span<TPTime>; 183 private pagination: Pagination; 184 private hasLogs = false; 185 private logFilteringCriteria?: LogFilteringCriteria; 186 private requestingData = false; 187 private queuedRunRequest = false; 188 189 constructor(args: LogsControllerArgs) { 190 super('main'); 191 this.engine = args.engine; 192 this.span = new TPTimeSpan(0n, BigInt(10e9)); 193 this.pagination = new Pagination(0, 0); 194 this.hasAnyLogs().then((exists) => { 195 this.hasLogs = exists; 196 publishTrackData({ 197 id: LogExistsKey, 198 data: { 199 exists, 200 }, 201 }); 202 }); 203 } 204 205 async hasAnyLogs() { 206 const result = await this.engine.query(` 207 select count(*) as cnt from android_logs 208 `); 209 return result.firstRow({cnt: NUM}).cnt > 0; 210 } 211 212 run() { 213 if (!this.hasLogs) return; 214 if (this.requestingData) { 215 this.queuedRunRequest = true; 216 return; 217 } 218 this.requestingData = true; 219 this.updateLogTracks().finally(() => { 220 this.requestingData = false; 221 if (this.queuedRunRequest) { 222 this.queuedRunRequest = false; 223 this.run(); 224 } 225 }); 226 } 227 228 private async updateLogTracks() { 229 const newSpan = globals.stateVisibleTime(); 230 const oldSpan = this.span; 231 232 const pagination = globals.state.logsPagination; 233 // This can occur when loading old traces. 234 // TODO(hjd): Fix the problem of accessing state from a previous version of 235 // the UI in a general way. 236 if (pagination === undefined) { 237 return; 238 } 239 240 const {offset, count} = pagination; 241 const requestedPagination = new Pagination(offset, count); 242 const oldPagination = this.pagination; 243 244 const newFilteringCriteria = 245 this.logFilteringCriteria !== globals.state.logFilteringCriteria; 246 const needBoundsUpdate = !oldSpan.equals(newSpan) || newFilteringCriteria; 247 const needEntriesUpdate = 248 !oldPagination.contains(requestedPagination) || needBoundsUpdate; 249 250 if (newFilteringCriteria) { 251 this.logFilteringCriteria = globals.state.logFilteringCriteria; 252 await this.engine.query('drop view if exists filtered_logs'); 253 254 const globMatch = LogsController.composeGlobMatch( 255 this.logFilteringCriteria.hideNonMatching, 256 this.logFilteringCriteria.textEntry); 257 let selectedRows = `select prio, ts, tag, msg, 258 process.name as process_name, ${globMatch} 259 from android_logs 260 left join thread using(utid) 261 left join process using(upid) 262 where prio >= ${this.logFilteringCriteria.minimumLevel}`; 263 if (this.logFilteringCriteria.tags.length) { 264 selectedRows += ` and tag in (${ 265 LogsController.serializeTags(this.logFilteringCriteria.tags)})`; 266 } 267 268 // We extract only the rows which will be visible. 269 await this.engine.query(`create view filtered_logs as select * 270 from (${selectedRows}) 271 where is_msg_chosen is 1 or is_process_chosen is 1`); 272 } 273 274 if (needBoundsUpdate) { 275 this.span = newSpan; 276 const logBounds = await updateLogBounds(this.engine, newSpan); 277 publishTrackData({ 278 id: LogBoundsKey, 279 data: logBounds, 280 }); 281 } 282 283 if (needEntriesUpdate) { 284 this.pagination = requestedPagination.grow(100); 285 const logEntries = 286 await updateLogEntries(this.engine, newSpan, this.pagination); 287 publishTrackData({ 288 id: LogEntriesKey, 289 data: logEntries, 290 }); 291 } 292 } 293 294 private static serializeTags(tags: string[]) { 295 return tags.map((tag) => escapeQuery(tag)).join(); 296 } 297 298 private static composeGlobMatch(isCollaped: boolean, textEntry: string) { 299 if (isCollaped) { 300 // If the entries are collapsed, we won't highlight any lines. 301 return `msg glob ${escapeGlob(textEntry)} as is_msg_chosen, 302 (process.name is not null and process.name glob ${ 303 escapeGlob(textEntry)}) as is_process_chosen, 304 0 as is_msg_highlighted, 305 0 as is_process_highlighted`; 306 } else if (!textEntry) { 307 // If there is no text entry, we will show all lines, but won't highlight. 308 // any. 309 return `1 as is_msg_chosen, 310 1 as is_process_chosen, 311 0 as is_msg_highlighted, 312 0 as is_process_highlighted`; 313 } else { 314 return `1 as is_msg_chosen, 315 1 as is_process_chosen, 316 msg glob ${escapeGlob(textEntry)} as is_msg_highlighted, 317 (process.name is not null and process.name glob ${ 318 escapeGlob(textEntry)}) as is_process_highlighted`; 319 } 320 } 321} 322