blob: 29f273a5ed41be09399e3cd35b096643498e728b [file] [log] [blame]
/*
* 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 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
import kotlinx.coroutines.flow.Flow
/**
* 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() }
// 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?) {
val change = obtain(timestamp, prefix, columnName)
change.set(value)
}
private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Boolean) {
val change = obtain(timestamp, prefix, columnName)
change.set(value)
}
private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Int?) {
val change = obtain(timestamp, prefix, columnName)
change.set(value)
}
// 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()
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"
)
}
}
@Synchronized
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.println(HEADER_PREFIX + name)
pw.println("version $VERSION")
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"