• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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