Merge changes I48a0acc7,I57fbba0a into tm-qpr-dev

* changes:
  [Table Logging] Save the last evicted value per column.
  [Table logging] Add tracing to the log methods.
This commit is contained in:
Caitlin Shkuratov
2023-03-09 17:47:45 +00:00
committed by Android (Google) Code Review
4 changed files with 361 additions and 2 deletions

View File

@@ -59,6 +59,17 @@ data class TableChange(
int = value int = value
} }
/** Updates this to store the same value as [change]. */
fun updateTo(change: TableChange) {
reset(change.timestamp, change.columnPrefix, change.columnName)
when (change.type) {
DataType.STRING -> set(change.str)
DataType.INT -> set(change.int)
DataType.BOOLEAN -> set(change.bool)
DataType.EMPTY -> {}
}
}
/** Returns true if this object has a change. */ /** Returns true if this object has a change. */
fun hasData(): Boolean { fun hasData(): Boolean {
return columnName.isNotBlank() && type != DataType.EMPTY return columnName.isNotBlank() && type != DataType.EMPTY

View File

@@ -16,13 +16,13 @@
package com.android.systemui.log.table package com.android.systemui.log.table
import android.os.Trace
import com.android.systemui.Dumpable import com.android.systemui.Dumpable
import com.android.systemui.plugins.util.RingBuffer import com.android.systemui.plugins.util.RingBuffer
import com.android.systemui.util.time.SystemClock import com.android.systemui.util.time.SystemClock
import java.io.PrintWriter import java.io.PrintWriter
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import kotlinx.coroutines.flow.Flow
/** /**
* A logger that logs changes in table format. * A logger that logs changes in table format.
@@ -82,6 +82,19 @@ class TableLogBuffer(
private val buffer = RingBuffer(maxSize) { TableChange() } 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. // A [TableRowLogger] object, re-used each time [logDiffs] is called.
// (Re-used to avoid object allocation.) // (Re-used to avoid object allocation.)
private val tempRow = TableRowLoggerImpl(0, columnPrefix = "", this) private val tempRow = TableRowLoggerImpl(0, columnPrefix = "", this)
@@ -138,18 +151,24 @@ class TableLogBuffer(
// timestamps.) // timestamps.)
private fun logChange(timestamp: Long, prefix: String, columnName: String, value: String?) { private fun logChange(timestamp: Long, prefix: String, columnName: String, value: String?) {
Trace.beginSection("TableLogBuffer#logChange(string)")
val change = obtain(timestamp, prefix, columnName) val change = obtain(timestamp, prefix, columnName)
change.set(value) change.set(value)
Trace.endSection()
} }
private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Boolean) { private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Boolean) {
Trace.beginSection("TableLogBuffer#logChange(boolean)")
val change = obtain(timestamp, prefix, columnName) val change = obtain(timestamp, prefix, columnName)
change.set(value) change.set(value)
Trace.endSection()
} }
private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Int?) { private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Int?) {
Trace.beginSection("TableLogBuffer#logChange(int)")
val change = obtain(timestamp, prefix, columnName) val change = obtain(timestamp, prefix, columnName)
change.set(value) change.set(value)
Trace.endSection()
} }
// TODO(b/259454430): Add additional change types here. // TODO(b/259454430): Add additional change types here.
@@ -158,6 +177,9 @@ class TableLogBuffer(
private fun obtain(timestamp: Long, prefix: String, columnName: String): TableChange { private fun obtain(timestamp: Long, prefix: String, columnName: String): TableChange {
verifyValidName(prefix, columnName) verifyValidName(prefix, columnName)
val tableChange = buffer.advance() val tableChange = buffer.advance()
if (tableChange.hasData()) {
saveEvictedValue(tableChange)
}
tableChange.reset(timestamp, prefix, columnName) tableChange.reset(timestamp, prefix, columnName)
return tableChange return tableChange
} }
@@ -173,10 +195,23 @@ class TableLogBuffer(
} }
} }
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 @Synchronized
override fun dump(pw: PrintWriter, args: Array<out String>) { override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.println(HEADER_PREFIX + name) pw.println(HEADER_PREFIX + name)
pw.println("version $VERSION") pw.println("version $VERSION")
lastEvictedValues.values.sortedBy { it.timestamp }.forEach { it.dump(pw) }
for (i in 0 until buffer.size) { for (i in 0 until buffer.size) {
buffer[i].dump(pw) buffer[i].dump(pw)
} }

View File

@@ -121,4 +121,92 @@ class TableChangeTest : SysuiTestCase() {
assertThat(underTest.getName()).doesNotContain("original") assertThat(underTest.getName()).doesNotContain("original")
assertThat(underTest.getVal()).isEqualTo("8900") assertThat(underTest.getVal()).isEqualTo("8900")
} }
@Test
fun updateTo_emptyToString_isString() {
val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName")
val new = TableChange(columnPrefix = "newPrefix", columnName = "newName")
new.set("newString")
underTest.updateTo(new)
assertThat(underTest.hasData()).isTrue()
assertThat(underTest.getName()).contains("newPrefix")
assertThat(underTest.getName()).contains("newName")
assertThat(underTest.getVal()).isEqualTo("newString")
}
@Test
fun updateTo_intToEmpty_isEmpty() {
val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName")
underTest.set(42)
val new = TableChange(columnPrefix = "newPrefix", columnName = "newName")
underTest.updateTo(new)
assertThat(underTest.hasData()).isFalse()
assertThat(underTest.getName()).contains("newPrefix")
assertThat(underTest.getName()).contains("newName")
assertThat(underTest.getVal()).isEqualTo("null")
}
@Test
fun updateTo_stringToBool_isBool() {
val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName")
underTest.set("oldString")
val new = TableChange(columnPrefix = "newPrefix", columnName = "newName")
new.set(true)
underTest.updateTo(new)
assertThat(underTest.hasData()).isTrue()
assertThat(underTest.getName()).contains("newPrefix")
assertThat(underTest.getName()).contains("newName")
assertThat(underTest.getVal()).isEqualTo("true")
}
@Test
fun updateTo_intToString_isString() {
val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName")
underTest.set(43)
val new = TableChange(columnPrefix = "newPrefix", columnName = "newName")
new.set("newString")
underTest.updateTo(new)
assertThat(underTest.hasData()).isTrue()
assertThat(underTest.getName()).contains("newPrefix")
assertThat(underTest.getName()).contains("newName")
assertThat(underTest.getVal()).isEqualTo("newString")
}
@Test
fun updateTo_boolToInt_isInt() {
val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName")
underTest.set(false)
val new = TableChange(columnPrefix = "newPrefix", columnName = "newName")
new.set(44)
underTest.updateTo(new)
assertThat(underTest.hasData()).isTrue()
assertThat(underTest.getName()).contains("newPrefix")
assertThat(underTest.getName()).contains("newName")
assertThat(underTest.getVal()).isEqualTo("44")
}
@Test
fun updateTo_boolToNewBool_isNewBool() {
val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName")
underTest.set(false)
val new = TableChange(columnPrefix = "newPrefix", columnName = "newName")
new.set(true)
underTest.updateTo(new)
assertThat(underTest.hasData()).isTrue()
assertThat(underTest.getName()).contains("newPrefix")
assertThat(underTest.getName()).contains("newName")
assertThat(underTest.getVal()).isEqualTo("true")
}
} }

View File

@@ -435,11 +435,236 @@ class TableLogBufferTest : SysuiTestCase() {
assertThat(dumpedString).doesNotContain("testString[0]") assertThat(dumpedString).doesNotContain("testString[0]")
assertThat(dumpedString).doesNotContain("testString[1]") assertThat(dumpedString).doesNotContain("testString[1]")
assertThat(dumpedString).doesNotContain("testString[2]") // The buffer should contain [MAX_SIZE + 1] entries since we also save the most recently
// evicted value.
assertThat(dumpedString).contains("testString[2]")
assertThat(dumpedString).contains("testString[3]") assertThat(dumpedString).contains("testString[3]")
assertThat(dumpedString).contains("testString[${MAX_SIZE + 2}]") assertThat(dumpedString).contains("testString[${MAX_SIZE + 2}]")
} }
@Test
fun columnEvicted_lastKnownColumnValueInDump() {
systemClock.setCurrentTimeMillis(100L)
underTest.logChange(prefix = "", columnName = "willBeEvicted", value = "evictedValue")
// Exactly fill the buffer so that "willBeEvicted" is evicted
for (i in 0 until MAX_SIZE) {
systemClock.advanceTime(100L)
val dumpString = "fillString[$i]"
underTest.logChange(prefix = "", columnName = "fillingColumn", value = dumpString)
}
val dumpedString = dumpChanges()
// Expect that we'll have both the evicted column entry...
val evictedColumnLog =
TABLE_LOG_DATE_FORMAT.format(100L) +
SEPARATOR +
"willBeEvicted" +
SEPARATOR +
"evictedValue"
assertThat(dumpedString).contains(evictedColumnLog)
// ... *and* all of the fillingColumn entries.
val firstFillingColumnLog =
TABLE_LOG_DATE_FORMAT.format(200L) +
SEPARATOR +
"fillingColumn" +
SEPARATOR +
"fillString[0]"
val lastFillingColumnLog =
TABLE_LOG_DATE_FORMAT.format(1100L) +
SEPARATOR +
"fillingColumn" +
SEPARATOR +
"fillString[9]"
assertThat(dumpedString).contains(firstFillingColumnLog)
assertThat(dumpedString).contains(lastFillingColumnLog)
}
@Test
fun multipleColumnsEvicted_allColumnsInDump() {
systemClock.setCurrentTimeMillis(100L)
underTest.logChange(prefix = "", columnName = "willBeEvictedString", value = "evictedValue")
systemClock.advanceTime(100L)
underTest.logChange(prefix = "", columnName = "willBeEvictedInt", value = 45)
systemClock.advanceTime(100L)
underTest.logChange(prefix = "", columnName = "willBeEvictedBool", value = true)
// Exactly fill the buffer so that all the above columns will be evicted
for (i in 0 until MAX_SIZE) {
systemClock.advanceTime(100L)
val dumpString = "fillString[$i]"
underTest.logChange(prefix = "", columnName = "fillingColumn", value = dumpString)
}
val dumpedString = dumpChanges()
// Expect that we'll have all the evicted column entries...
val evictedColumnLogString =
TABLE_LOG_DATE_FORMAT.format(100L) +
SEPARATOR +
"willBeEvictedString" +
SEPARATOR +
"evictedValue"
val evictedColumnLogInt =
TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + "willBeEvictedInt" + SEPARATOR + "45"
val evictedColumnLogBool =
TABLE_LOG_DATE_FORMAT.format(300L) +
SEPARATOR +
"willBeEvictedBool" +
SEPARATOR +
"true"
assertThat(dumpedString).contains(evictedColumnLogString)
assertThat(dumpedString).contains(evictedColumnLogInt)
assertThat(dumpedString).contains(evictedColumnLogBool)
// ... *and* all of the fillingColumn entries.
val firstFillingColumnLog =
TABLE_LOG_DATE_FORMAT.format(400) +
SEPARATOR +
"fillingColumn" +
SEPARATOR +
"fillString[0]"
val lastFillingColumnLog =
TABLE_LOG_DATE_FORMAT.format(1300) +
SEPARATOR +
"fillingColumn" +
SEPARATOR +
"fillString[9]"
assertThat(dumpedString).contains(firstFillingColumnLog)
assertThat(dumpedString).contains(lastFillingColumnLog)
}
@Test
fun multipleColumnsEvicted_differentPrefixSameName_allColumnsInDump() {
systemClock.setCurrentTimeMillis(100L)
underTest.logChange(prefix = "prefix1", columnName = "sameName", value = "value1")
systemClock.advanceTime(100L)
underTest.logChange(prefix = "prefix2", columnName = "sameName", value = "value2")
systemClock.advanceTime(100L)
underTest.logChange(prefix = "prefix3", columnName = "sameName", value = "value3")
// Exactly fill the buffer so that all the above columns will be evicted
for (i in 0 until MAX_SIZE) {
systemClock.advanceTime(100L)
val dumpString = "fillString[$i]"
underTest.logChange(prefix = "", columnName = "fillingColumn", value = dumpString)
}
val dumpedString = dumpChanges()
// Expect that we'll have all the evicted column entries
val evictedColumn1 =
TABLE_LOG_DATE_FORMAT.format(100L) +
SEPARATOR +
"prefix1.sameName" +
SEPARATOR +
"value1"
val evictedColumn2 =
TABLE_LOG_DATE_FORMAT.format(200L) +
SEPARATOR +
"prefix2.sameName" +
SEPARATOR +
"value2"
val evictedColumn3 =
TABLE_LOG_DATE_FORMAT.format(300L) +
SEPARATOR +
"prefix3.sameName" +
SEPARATOR +
"value3"
assertThat(dumpedString).contains(evictedColumn1)
assertThat(dumpedString).contains(evictedColumn2)
assertThat(dumpedString).contains(evictedColumn3)
}
@Test
fun multipleColumnsEvicted_dumpSortedByTimestamp() {
systemClock.setCurrentTimeMillis(100L)
underTest.logChange(prefix = "", columnName = "willBeEvictedFirst", value = "evictedValue")
systemClock.advanceTime(100L)
underTest.logChange(prefix = "", columnName = "willBeEvictedSecond", value = 45)
systemClock.advanceTime(100L)
underTest.logChange(prefix = "", columnName = "willBeEvictedThird", value = true)
// Exactly fill the buffer with so that all the above columns will be evicted
for (i in 0 until MAX_SIZE) {
systemClock.advanceTime(100L)
val dumpString = "fillString[$i]"
underTest.logChange(prefix = "", columnName = "fillingColumn", value = dumpString)
}
val dumpedString = dumpChanges()
// Expect that we'll have all the evicted column entries in timestamp order
val firstEvictedLog =
TABLE_LOG_DATE_FORMAT.format(100L) +
SEPARATOR +
"willBeEvictedFirst" +
SEPARATOR +
"evictedValue"
val secondEvictedLog =
TABLE_LOG_DATE_FORMAT.format(200L) +
SEPARATOR +
"willBeEvictedSecond" +
SEPARATOR +
"45"
val thirdEvictedLog =
TABLE_LOG_DATE_FORMAT.format(300L) +
SEPARATOR +
"willBeEvictedThird" +
SEPARATOR +
"true"
assertThat(dumpedString).contains(firstEvictedLog)
val stringAfterFirst = dumpedString.substringAfter(firstEvictedLog)
assertThat(stringAfterFirst).contains(secondEvictedLog)
val stringAfterSecond = stringAfterFirst.substringAfter(secondEvictedLog)
assertThat(stringAfterSecond).contains(thirdEvictedLog)
}
@Test
fun sameColumnEvictedMultipleTimes_onlyLastEvictionInDump() {
systemClock.setCurrentTimeMillis(0L)
for (i in 1 until 4) {
systemClock.advanceTime(100L)
val dumpString = "evicted[$i]"
underTest.logChange(prefix = "", columnName = "evictedColumn", value = dumpString)
}
// Exactly fill the buffer so that all the entries for "evictedColumn" will be evicted.
for (i in 0 until MAX_SIZE) {
systemClock.advanceTime(100L)
val dumpString = "fillString[$i]"
underTest.logChange(prefix = "", columnName = "fillingColumn", value = dumpString)
}
val dumpedString = dumpChanges()
// Expect that we only have the most recent evicted column entry
val evictedColumnLog1 =
TABLE_LOG_DATE_FORMAT.format(100L) +
SEPARATOR +
"evictedColumn" +
SEPARATOR +
"evicted[1]"
val evictedColumnLog2 =
TABLE_LOG_DATE_FORMAT.format(200L) +
SEPARATOR +
"evictedColumn" +
SEPARATOR +
"evicted[2]"
val evictedColumnLog3 =
TABLE_LOG_DATE_FORMAT.format(300L) +
SEPARATOR +
"evictedColumn" +
SEPARATOR +
"evicted[3]"
assertThat(dumpedString).doesNotContain(evictedColumnLog1)
assertThat(dumpedString).doesNotContain(evictedColumnLog2)
assertThat(dumpedString).contains(evictedColumnLog3)
}
private fun dumpChanges(): String { private fun dumpChanges(): String {
underTest.dump(PrintWriter(outputWriter), arrayOf()) underTest.dump(PrintWriter(outputWriter), arrayOf())
return outputWriter.toString() return outputWriter.toString()