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