From 9755968481dc84ca0d9586c80a8d5f7405ed50d1 Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Fri, 18 Feb 2022 10:19:02 -0500 Subject: [PATCH] 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 --- .../systemui/dump/DumpsysTableLogger.kt | 125 +++++++++++++++ .../connectivity/ConnectivityState.kt | 28 ++++ .../connectivity/MobileSignalController.java | 2 + .../statusbar/connectivity/MobileState.kt | 46 ++++++ .../connectivity/SignalController.java | 71 +++++++-- .../connectivity/WifiSignalController.java | 1 + .../statusbar/connectivity/WifiState.kt | 24 +++ .../systemui/dump/DumpsysTableLoggerTest.kt | 146 ++++++++++++++++++ 8 files changed, 433 insertions(+), 10 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/dump/DumpsysTableLogger.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/dump/DumpsysTableLoggerTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpsysTableLogger.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpsysTableLogger.kt new file mode 100644 index 0000000000000..f7e6b98a0f064 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpsysTableLogger.kt @@ -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` 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: + * version 1 + * col1|col2|...|colN + * field10|field20|...|fieldN0 + * //... + * field1M|field2M|...|fieldNM + * SystemUI TableSection END: + * ``` + * + * @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, + private val rows: List +) { + + 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 + +/** + * 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" \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityState.kt index 9c3c10c9219ba..b66e17588c105 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityState.kt @@ -46,6 +46,34 @@ open class ConnectivityState { } } + protected open fun tableColumns(): List { + return listOf( + "connected", + "enabled", + "activityIn", + "activityOut", + "level", + "iconGroup", + "inetCondition", + "rssi", + "time") + } + + protected open fun tableData(): List { + 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 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java index 41d2b655d8052..9d8667a3ccb08 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java @@ -828,6 +828,8 @@ public class MobileSignalController extends SignalController { + 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 { + 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 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/SignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/SignalController.java index cd2006899cfc6..e2806a39130fe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/SignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/SignalController.java @@ -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= mHistoryIndex + HISTORY_SIZE - size; i--) { - pw.println(" Previous State(" + (mHistoryIndex + HISTORY_SIZE - i) + "): " - + mHistory[i & (HISTORY_SIZE - 1)]); + List 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 getOrderedHistoryExcludingCurrentState() { + ArrayList 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 getOrderedHistory() { + ArrayList history = new ArrayList<>(); + // Start with the current state + history.add(mCurrentState); + history.addAll(getOrderedHistoryExcludingCurrentState()); + return history; + } + + void dumpTableData(PrintWriter pw) { + List> tableData = new ArrayList>(); + List 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); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java index b80df4ab394af..a4589c8dd6d36 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java @@ -241,6 +241,7 @@ public class WifiSignalController extends SignalController public void dump(PrintWriter pw) { super.dump(pw); mWifiTracker.dump(pw); + dumpTableData(pw); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiState.kt index ac15f78191f68..d32e34915c612 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiState.kt @@ -48,6 +48,30 @@ internal class WifiState( .append(",subId=").append(subId) } + override fun tableColumns(): List { + val columns = listOf("ssid", + "isTransient", + "isDefault", + "statusLabel", + "isCarrierMerged", + "subId") + + return super.tableColumns() + columns + } + + override fun tableData(): List { + 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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpsysTableLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpsysTableLoggerTest.kt new file mode 100644 index 0000000000000..1d2afe4047c34 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpsysTableLoggerTest.kt @@ -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 { + 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" + } +} \ No newline at end of file