[Table Logging] Save the last evicted value per column.
Fixes: 272016422 Test: atest TableLogBufferTest TableChangeTest Test: systraces to verify the saving doesn't vastly increase log time. Verify that `wifiEnabled` still shows up in the table log. Test: Keep changing wifi networks (but leave wifi enabled always). See bug for more information, but tl;dr: Average log takes 35us. Change-Id: I48a0acc7b1191e3b3262e0401991afe76099b1a4
This commit is contained in:
@@ -59,6 +59,17 @@ data class TableChange(
|
||||
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. */
|
||||
fun hasData(): Boolean {
|
||||
return columnName.isNotBlank() && type != DataType.EMPTY
|
||||
|
||||
@@ -82,6 +82,19 @@ class TableLogBuffer(
|
||||
|
||||
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)
|
||||
@@ -164,6 +177,9 @@ class TableLogBuffer(
|
||||
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
|
||||
}
|
||||
@@ -179,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
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -121,4 +121,92 @@ class TableChangeTest : SysuiTestCase() {
|
||||
assertThat(underTest.getName()).doesNotContain("original")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,11 +435,236 @@ class TableLogBufferTest : SysuiTestCase() {
|
||||
|
||||
assertThat(dumpedString).doesNotContain("testString[0]")
|
||||
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[${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 {
|
||||
underTest.dump(PrintWriter(outputWriter), arrayOf())
|
||||
return outputWriter.toString()
|
||||
|
||||
Reference in New Issue
Block a user