1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.log.table 18 19 import android.os.Trace 20 import com.android.systemui.Dumpable 21 import com.android.systemui.plugins.util.RingBuffer 22 import com.android.systemui.util.time.SystemClock 23 import java.io.PrintWriter 24 import java.text.SimpleDateFormat 25 import java.util.Locale 26 27 /** 28 * A logger that logs changes in table format. 29 * 30 * Some parts of System UI maintain a lot of pieces of state at once. 31 * [com.android.systemui.plugins.log.LogBuffer] allows us to easily log change events: 32 * - 10-10 10:10:10.456: state2 updated to newVal2 33 * - 10-10 10:11:00.000: stateN updated to StateN(val1=true, val2=1) 34 * - 10-10 10:11:02.123: stateN updated to StateN(val1=true, val2=2) 35 * - 10-10 10:11:05.123: state1 updated to newVal1 36 * - 10-10 10:11:06.000: stateN updated to StateN(val1=false, val2=3) 37 * 38 * However, it can sometimes be more useful to view the state changes in table format: 39 * - timestamp--------- | state1- | state2- | ... | stateN.val1 | stateN.val2 40 * - ------------------------------------------------------------------------- 41 * - 10-10 10:10:10.123 | val1--- | val2--- | ... | false------ | 0----------- 42 * - 10-10 10:10:10.456 | val1--- | newVal2 | ... | false------ | 0----------- 43 * - 10-10 10:11:00.000 | val1--- | newVal2 | ... | true------- | 1----------- 44 * - 10-10 10:11:02.123 | val1--- | newVal2 | ... | true------- | 2----------- 45 * - 10-10 10:11:05.123 | newVal1 | newVal2 | ... | true------- | 2----------- 46 * - 10-10 10:11:06.000 | newVal1 | newVal2 | ... | false------ | 3----------- 47 * 48 * This class enables easy logging of the state changes in both change event format and table 49 * format. 50 * 51 * This class also enables easy logging of states that are a collection of fields. For example, 52 * stateN in the above example consists of two fields -- val1 and val2. It's useful to put each 53 * field into its own column so that ABT (Android Bug Tool) can easily highlight changes to 54 * individual fields. 55 * 56 * How it works: 57 * 1) Create an instance of this buffer via [TableLogBufferFactory]. 58 * 2) For any states being logged, implement [Diffable]. Implementing [Diffable] allows the state to 59 * only log the fields that have *changed* since the previous update, instead of always logging 60 * all fields. 61 * 3) Each time a change in a state happens, call [logDiffs]. If your state is emitted using a 62 * [Flow], you should use the [logDiffsForTable] extension function to automatically log diffs 63 * any time your flow emits a new value. 64 * 65 * When a dump occurs, there will be two dumps: 66 * 1) The change events under the dumpable name "$name-changes". 67 * 2) This class will coalesce all the diffs into a table format and log them under the dumpable 68 * name "$name-table". 69 * 70 * @param maxSize the maximum size of the buffer. Must be > 0. 71 */ 72 class TableLogBuffer( 73 maxSize: Int, 74 private val name: String, 75 private val systemClock: SystemClock, 76 ) : Dumpable { 77 init { 78 if (maxSize <= 0) { 79 throw IllegalArgumentException("maxSize must be > 0") 80 } 81 } 82 <lambda>null83 private val buffer = RingBuffer(maxSize) { TableChange() } 84 85 // Stores the most recently evicted value for each column name (indexed on column name). 86 // 87 // Why it's necessary: Because we use a RingBuffer of a fixed size, it's possible that a column 88 // that's logged infrequently will eventually get pushed out by a different column that's 89 // logged more frequently. Then, that infrequently-logged column isn't present in the RingBuffer 90 // at all and we have no logs that the column ever existed. This is a problem because the 91 // column's information is still relevant, valid, and may be critical to debugging issues. 92 // 93 // Fix: When a change is being evicted from the RingBuffer, we store it in this map (based on 94 // its [TableChange.getName()]. This ensures that we always have at least one value for every 95 // column ever logged. See b/272016422 for more details. 96 private val lastEvictedValues = mutableMapOf<String, TableChange>() 97 98 // A [TableRowLogger] object, re-used each time [logDiffs] is called. 99 // (Re-used to avoid object allocation.) 100 private val tempRow = TableRowLoggerImpl(0, columnPrefix = "", this) 101 102 /** 103 * Log the differences between [prevVal] and [newVal]. 104 * 105 * The [newVal] object's method [Diffable.logDiffs] will be used to fetch the diffs. 106 * 107 * @param columnPrefix a prefix that will be applied to every column name that gets logged. This 108 * ensures that all the columns related to the same state object will be grouped together in 109 * the table. 110 * @throws IllegalArgumentException if [columnPrefix] or column name contain "|". "|" is used as 111 * the separator token for parsing, so it can't be present in any part of the column name. 112 */ 113 @Synchronized logDiffsnull114 fun <T : Diffable<T>> logDiffs(columnPrefix: String, prevVal: T, newVal: T) { 115 val row = tempRow 116 row.timestamp = systemClock.currentTimeMillis() 117 row.columnPrefix = columnPrefix 118 newVal.logDiffs(prevVal, row) 119 } 120 121 /** 122 * Logs change(s) to the buffer using [rowInitializer]. 123 * 124 * @param rowInitializer a function that will be called immediately to store relevant data on 125 * the row. 126 */ 127 @Synchronized logChangenull128 fun logChange(columnPrefix: String, rowInitializer: (TableRowLogger) -> Unit) { 129 val row = tempRow 130 row.timestamp = systemClock.currentTimeMillis() 131 row.columnPrefix = columnPrefix 132 rowInitializer(row) 133 } 134 135 /** Logs a String? change. */ logChangenull136 fun logChange(prefix: String, columnName: String, value: String?) { 137 logChange(systemClock.currentTimeMillis(), prefix, columnName, value) 138 } 139 140 /** Logs a boolean change. */ logChangenull141 fun logChange(prefix: String, columnName: String, value: Boolean) { 142 logChange(systemClock.currentTimeMillis(), prefix, columnName, value) 143 } 144 145 /** Logs a Int change. */ logChangenull146 fun logChange(prefix: String, columnName: String, value: Int?) { 147 logChange(systemClock.currentTimeMillis(), prefix, columnName, value) 148 } 149 150 // Keep these individual [logChange] methods private (don't let clients give us their own 151 // timestamps.) 152 logChangenull153 private fun logChange(timestamp: Long, prefix: String, columnName: String, value: String?) { 154 Trace.beginSection("TableLogBuffer#logChange(string)") 155 val change = obtain(timestamp, prefix, columnName) 156 change.set(value) 157 Trace.endSection() 158 } 159 logChangenull160 private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Boolean) { 161 Trace.beginSection("TableLogBuffer#logChange(boolean)") 162 val change = obtain(timestamp, prefix, columnName) 163 change.set(value) 164 Trace.endSection() 165 } 166 logChangenull167 private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Int?) { 168 Trace.beginSection("TableLogBuffer#logChange(int)") 169 val change = obtain(timestamp, prefix, columnName) 170 change.set(value) 171 Trace.endSection() 172 } 173 174 // TODO(b/259454430): Add additional change types here. 175 176 @Synchronized obtainnull177 private fun obtain(timestamp: Long, prefix: String, columnName: String): TableChange { 178 verifyValidName(prefix, columnName) 179 val tableChange = buffer.advance() 180 if (tableChange.hasData()) { 181 saveEvictedValue(tableChange) 182 } 183 tableChange.reset(timestamp, prefix, columnName) 184 return tableChange 185 } 186 verifyValidNamenull187 private fun verifyValidName(prefix: String, columnName: String) { 188 if (prefix.contains(SEPARATOR)) { 189 throw IllegalArgumentException("columnPrefix cannot contain $SEPARATOR but was $prefix") 190 } 191 if (columnName.contains(SEPARATOR)) { 192 throw IllegalArgumentException( 193 "columnName cannot contain $SEPARATOR but was $columnName" 194 ) 195 } 196 } 197 saveEvictedValuenull198 private fun saveEvictedValue(change: TableChange) { 199 Trace.beginSection("TableLogBuffer#saveEvictedValue") 200 val name = change.getName() 201 val previouslyEvicted = 202 lastEvictedValues[name] ?: TableChange().also { lastEvictedValues[name] = it } 203 // For recycling purposes, update the existing object in the map with the new information 204 // instead of creating a new object. 205 previouslyEvicted.updateTo(change) 206 Trace.endSection() 207 } 208 209 @Synchronized dumpnull210 override fun dump(pw: PrintWriter, args: Array<out String>) { 211 pw.println(HEADER_PREFIX + name) 212 pw.println("version $VERSION") 213 214 lastEvictedValues.values.sortedBy { it.timestamp }.forEach { it.dump(pw) } 215 for (i in 0 until buffer.size) { 216 buffer[i].dump(pw) 217 } 218 pw.println(FOOTER_PREFIX + name) 219 } 220 221 /** Dumps an individual [TableChange]. */ dumpnull222 private fun TableChange.dump(pw: PrintWriter) { 223 if (!this.hasData()) { 224 return 225 } 226 val formattedTimestamp = TABLE_LOG_DATE_FORMAT.format(timestamp) 227 pw.print(formattedTimestamp) 228 pw.print(SEPARATOR) 229 pw.print(this.getName()) 230 pw.print(SEPARATOR) 231 pw.print(this.getVal()) 232 pw.println() 233 } 234 235 /** 236 * A private implementation of [TableRowLogger]. 237 * 238 * Used so that external clients can't modify [timestamp]. 239 */ 240 private class TableRowLoggerImpl( 241 var timestamp: Long, 242 var columnPrefix: String, 243 val tableLogBuffer: TableLogBuffer, 244 ) : TableRowLogger { 245 /** Logs a change to a string value. */ logChangenull246 override fun logChange(columnName: String, value: String?) { 247 tableLogBuffer.logChange(timestamp, columnPrefix, columnName, value) 248 } 249 250 /** Logs a change to a boolean value. */ logChangenull251 override fun logChange(columnName: String, value: Boolean) { 252 tableLogBuffer.logChange(timestamp, columnPrefix, columnName, value) 253 } 254 255 /** Logs a change to an int value. */ logChangenull256 override fun logChange(columnName: String, value: Int) { 257 tableLogBuffer.logChange(timestamp, columnPrefix, columnName, value) 258 } 259 } 260 } 261 262 val TABLE_LOG_DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) 263 264 private const val HEADER_PREFIX = "SystemUI StateChangeTableSection START: " 265 private const val FOOTER_PREFIX = "SystemUI StateChangeTableSection END: " 266 private const val SEPARATOR = "|" 267 private const val VERSION = "1" 268