Add a table data logger for ABT

This CL adds support for a simple, tabular-data logger for dumpsys to be
consumed by something that can pretty-print the data.

Test: atest DumpsysTableLoggerTest
Bug: 220386889
Change-Id: I2db7d6891d4d9a18b72b95432c053efa2d49c2e6
This commit is contained in:
Evan Laird
2022-02-18 10:19:02 -05:00
parent 3a9b1b828f
commit 9755968481
8 changed files with 433 additions and 10 deletions

View File

@@ -0,0 +1,125 @@
/*
* 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.dump
import java.io.PrintWriter
/**
* Utility for logging nice table data to be parsed (and pretty printed) in bugreports. The general
* idea here is to feed your nice, table-like data to this class, which embeds the schema and rows
* into the dumpsys, wrapped in a known start and stop tags. Later, one can build a simple parser
* and pretty-print this data in a table
*
* Note: Something should be said here about silently eating errors by filtering out malformed
* lines. Because this class is expected to be utilized only during a dumpsys, it doesn't feel
* most correct to throw an exception here (since an exception can often be the reason that this
* class is created). Because of this, [DumpsysTableLogger] will simply filter out invalid lines
* based solely on line length. This behavior might need to be revisited in the future.
*
* USAGE:
* Assuming we have some data that would be logged to dumpsys like so:
*
* ```
* 1: field1=val1, field2=val2..., fieldN=valN
* //...
* M: field1M=val1M, ..., fieldNM
* ```
*
* You can break the `field<n>` values out into a columns spec:
* ```
* val cols = [field1, field2,...,fieldN]
* ```
* And then take all of the historical data lines (1 through M), and break them out into their own
* lists:
* ```
* val rows = [
* [field10, field20,..., fieldN0],
* //...
* [field1M, field2M,..., fieldNM]
* ]
* ```
*
* Lastly, create a bugreport-unique section name, and use the table logger to write the data to
* dumpsys:
* ```
* val logger = DumpsysTableLogger(uniqueName, cols, rows)
* logger.printTableData(pw)
* ```
*
* The expected output in the dumpsys would be:
* ```
* SystemUI TableSection START: <SectionName>
* version 1
* col1|col2|...|colN
* field10|field20|...|fieldN0
* //...
* field1M|field2M|...|fieldNM
* SystemUI TableSection END: <SectionName>
* ```
*
* @param sectionName A name for the table data section. Should be unique in the bugreport
* @param columns Definition for the columns of the table. This should be the same length as all
* data rows
* @param rows List of rows to be displayed in the table
*/
class DumpsysTableLogger(
private val sectionName: String,
private val columns: List<String>,
private val rows: List<Row>
) {
fun printTableData(pw: PrintWriter) {
printSectionStart(pw)
printSchema(pw)
printData(pw)
printSectionEnd(pw)
}
private fun printSectionStart(pw: PrintWriter) {
pw.println(HEADER_PREFIX + sectionName)
pw.println("version $VERSION")
}
private fun printSectionEnd(pw: PrintWriter) {
pw.println(FOOTER_PREFIX + sectionName)
}
private fun printSchema(pw: PrintWriter) {
pw.println(columns.joinToString(separator = SEPARATOR))
}
private fun printData(pw: PrintWriter) {
val count = columns.size
rows
.filter { it.size == count }
.forEach { dataLine ->
pw.println(dataLine.joinToString(separator = SEPARATOR))
}
}
}
typealias Row = List<String>
/**
* DO NOT CHANGE! (but if you must...)
* 1. Update the version number
* 2. Update any consumers to parse the new version
*/
private const val HEADER_PREFIX = "SystemUI TableSection START: "
private const val FOOTER_PREFIX = "SystemUI TableSection END: "
private const val SEPARATOR = "|" // TBD
private const val VERSION = "1"

View File

@@ -46,6 +46,34 @@ open class ConnectivityState {
}
}
protected open fun tableColumns(): List<String> {
return listOf(
"connected",
"enabled",
"activityIn",
"activityOut",
"level",
"iconGroup",
"inetCondition",
"rssi",
"time")
}
protected open fun tableData(): List<String> {
return listOf(
connected,
enabled,
activityIn,
activityOut,
level,
iconGroup,
inetCondition,
rssi,
sSDF.format(time)).map {
it.toString()
}
}
protected open fun copyFrom(other: ConnectivityState) {
connected = other.connected
enabled = other.enabled

View File

@@ -828,6 +828,8 @@ public class MobileSignalController extends SignalController<MobileState, Mobile
+ (mMobileStatusHistoryIndex + STATUS_HISTORY_SIZE - i) + "): "
+ mMobileStatusHistory[i & (STATUS_HISTORY_SIZE - 1)]);
}
dumpTableData(pw);
}
/** Box for QS icon info */

View File

@@ -162,6 +162,52 @@ internal class MobileState(
builder.append("displayInfo=$telephonyDisplayInfo")
}
override fun tableColumns(): List<String> {
val columns = listOf("dataSim",
"networkName",
"networkNameData",
"dataConnected",
"roaming",
"isDefault",
"isEmergency",
"airplaneMode",
"carrierNetworkChangeMode",
"userSetup",
"dataState",
"defaultDataOff",
"showQuickSettingsRatIcon",
"voiceServiceState",
"isInService",
"serviceState",
"signalStrength",
"displayInfo")
return super.tableColumns() + columns
}
override fun tableData(): List<String> {
val columns = listOf(dataSim,
networkName,
networkNameData,
dataConnected,
roaming,
isDefault,
isEmergency,
airplaneMode,
carrierNetworkChangeMode,
userSetup,
dataState,
defaultDataOff,
showQuickSettingsRatIcon(),
getVoiceServiceState(),
isInService(),
serviceState?.minLog() ?: "(null)",
signalStrength?.minLog() ?: "(null)",
telephonyDisplayInfo).map { it.toString() }
return super.tableData() + columns
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View File

@@ -22,9 +22,12 @@ import android.content.Context;
import android.util.Log;
import com.android.settingslib.SignalIcon.IconGroup;
import com.android.systemui.dump.DumpsysTableLogger;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
/**
@@ -193,20 +196,68 @@ public abstract class SignalController<T extends ConnectivityState, I extends Ic
pw.println(" - " + mTag + " -----");
pw.println(" Current State: " + mCurrentState);
if (RECORD_HISTORY) {
// Count up the states that actually contain time stamps, and only display those.
int size = 0;
for (int i = 0; i < HISTORY_SIZE; i++) {
if (mHistory[i].time != 0) size++;
}
// Print out the previous states in ordered number.
for (int i = mHistoryIndex + HISTORY_SIZE - 1;
i >= mHistoryIndex + HISTORY_SIZE - size; i--) {
pw.println(" Previous State(" + (mHistoryIndex + HISTORY_SIZE - i) + "): "
+ mHistory[i & (HISTORY_SIZE - 1)]);
List<ConnectivityState> history = getOrderedHistoryExcludingCurrentState();
for (int i = 0; i < history.size(); i++) {
pw.println(" Previous State(" + (i + 1) + "): " + mHistory[i]);
}
}
}
/**
* mHistory is a ring, so use this method to get the time-ordered (from youngest to oldest)
* list of historical states. Filters out any state whose `time` is `0`.
*
* For ease of compatibility, this list returns JUST the historical states, not the current
* state which has yet to be copied into the history
*
* @see #getOrderedHistory()
* @return historical states, ordered from newest to oldest
*/
List<ConnectivityState> getOrderedHistoryExcludingCurrentState() {
ArrayList<ConnectivityState> history = new ArrayList<>();
// Count up the states that actually contain time stamps, and only display those.
int size = 0;
for (int i = 0; i < HISTORY_SIZE; i++) {
if (mHistory[i].time != 0) size++;
}
// Print out the previous states in ordered number.
for (int i = mHistoryIndex + HISTORY_SIZE - 1;
i >= mHistoryIndex + HISTORY_SIZE - size; i--) {
history.add(mHistory[i & (HISTORY_SIZE - 1)]);
}
return history;
}
/**
* Get the ordered history states, including the current yet-to-be-copied state. Useful for
* logging
*
* @see #getOrderedHistoryExcludingCurrentState()
* @return [currentState, historicalState...] array
*/
List<ConnectivityState> getOrderedHistory() {
ArrayList<ConnectivityState> history = new ArrayList<>();
// Start with the current state
history.add(mCurrentState);
history.addAll(getOrderedHistoryExcludingCurrentState());
return history;
}
void dumpTableData(PrintWriter pw) {
List<List<String>> tableData = new ArrayList<List<String>>();
List<ConnectivityState> history = getOrderedHistory();
for (int i = 0; i < history.size(); i++) {
tableData.add(history.get(i).tableData());
}
DumpsysTableLogger logger =
new DumpsysTableLogger(mTag, mCurrentState.tableColumns(), tableData);
logger.printTableData(pw);
}
final void notifyListeners() {
notifyListeners(mCallbackHandler);
}

View File

@@ -241,6 +241,7 @@ public class WifiSignalController extends SignalController<WifiState, IconGroup>
public void dump(PrintWriter pw) {
super.dump(pw);
mWifiTracker.dump(pw);
dumpTableData(pw);
}
/**

View File

@@ -48,6 +48,30 @@ internal class WifiState(
.append(",subId=").append(subId)
}
override fun tableColumns(): List<String> {
val columns = listOf("ssid",
"isTransient",
"isDefault",
"statusLabel",
"isCarrierMerged",
"subId")
return super.tableColumns() + columns
}
override fun tableData(): List<String> {
val data = listOf(ssid,
isTransient,
isDefault,
statusLabel,
isCarrierMerged,
subId).map {
it.toString()
}
return super.tableData() + data
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View File

@@ -0,0 +1,146 @@
/*
* 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.dump
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import java.io.PrintWriter
import java.io.StringWriter
@SmallTest
class DumpsysTableLoggerTest : SysuiTestCase() {
private val logger = DumpsysTableLogger(
TEST_SECTION_NAME,
TEST_COLUMNS,
TEST_DATA_VALID)
private val stringWriter = StringWriter()
private val printWriter = PrintWriter(stringWriter)
@Before
fun setup() {
}
@Test
fun testTableLogger_header() {
logger.printTableData(printWriter)
val lines = logLines(stringWriter)
val line1 = lines[0]
assertEquals("table logger header is incorrect",
HEADER_PREFIX + TEST_SECTION_NAME, line1)
}
@Test
fun testTableLogger_version() {
logger.printTableData(printWriter)
val lines = logLines(stringWriter)
val line2 = lines[1]
assertEquals("version probably shouldn't have changed",
"version $VERSION", line2)
}
@Test
fun testTableLogger_footer() {
logger.printTableData(printWriter)
val lines = logLines(stringWriter)
val footer = lines.last()
android.util.Log.d("evanevan", footer)
android.util.Log.d("evanevan", lines.toString())
assertEquals("table logger footer is incorrect",
FOOTER_PREFIX + TEST_SECTION_NAME, footer)
}
@Test
fun testTableLogger_data_length() {
logger.printTableData(printWriter)
val lines = logLines(stringWriter)
// Header is 2 lines long, plus a line for the column defs so data is lines[3..last()-1]
val data = lines.subList(3, lines.size - 1)
assertEquals(TEST_DATA_LENGTH, data.size)
}
@Test
fun testTableLogger_data_columns() {
logger.printTableData(printWriter)
val lines = logLines(stringWriter)
// Header is always 2 lines long so data is lines[2..last()-1]
val data = lines.subList(3, lines.size - 1)
data.forEach { dataLine ->
assertEquals(TEST_COLUMNS.size, dataLine.split(SEPARATOR).size)
}
}
@Test
fun testInvalidLinesAreFiltered() {
// GIVEN an invalid data row, by virtue of having an extra field
val invalidLine = List(TEST_COLUMNS.size) { col ->
"data${col}X"
} + "INVALID COLUMN"
val invalidData = TEST_DATA_VALID.toMutableList().also {
it.add(invalidLine)
}
// WHEN the table logger is created and asked to print the table
val tableLogger = DumpsysTableLogger(
TEST_SECTION_NAME,
TEST_COLUMNS,
invalidData)
tableLogger.printTableData(printWriter)
// THEN the invalid line is filtered out
val invalidString = invalidLine.joinToString(separator = SEPARATOR)
val logString = stringWriter.toString()
assertThat(logString).doesNotContain(invalidString)
}
private fun logLines(sw: StringWriter): List<String> {
return sw.toString().split("\n").filter { it.isNotBlank() }
}
}
// Copying these here from [DumpsysTableLogger] so that we catch any accidental versioning change
private const val HEADER_PREFIX = "SystemUI TableSection START: "
private const val FOOTER_PREFIX = "SystemUI TableSection END: "
private const val SEPARATOR = "|" // TBD
private const val VERSION = "1"
const val TEST_SECTION_NAME = "TestTableSection"
const val TEST_DATA_LENGTH = 5
val TEST_COLUMNS = arrayListOf("col1", "col2", "col3")
val TEST_DATA_VALID = List(TEST_DATA_LENGTH) { row ->
List(TEST_COLUMNS.size) { col ->
"data$col$row"
}
}