Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 931c953e authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Automerger Merge Worker
Browse files

Merge "[SB Refactor] Implement a table log buffer." into tm-qpr-dev am: 36c8d868

parents 51c742fa 36c8d868
Loading
Loading
Loading
Loading
+1 −10
Original line number Diff line number Diff line
@@ -16,9 +16,9 @@

package com.android.systemui.log

import android.app.ActivityManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.log.LogBufferHelper.Companion.adjustMaxSize
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogcatEchoTracker

@@ -29,15 +29,6 @@ class LogBufferFactory @Inject constructor(
    private val dumpManager: DumpManager,
    private val logcatEchoTracker: LogcatEchoTracker
) {
    /* limitiometricMessageDeferralLogger the size of maxPoolSize for low ram (Go) devices */
    private fun adjustMaxSize(requestedMaxSize: Int): Int {
        return if (ActivityManager.isLowRamDeviceStatic()) {
            minOf(requestedMaxSize, 20) /* low ram max log size*/
        } else {
            requestedMaxSize
        }
    }

    @JvmOverloads
    fun create(
        name: String,
+32 −0
Original line number Diff line number Diff line
/*
 * 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

import android.app.ActivityManager

class LogBufferHelper {
    companion object {
        /** If necessary, returns a limited maximum size for low ram (Go) devices */
        fun adjustMaxSize(requestedMaxSize: Int): Int {
            return if (ActivityManager.isLowRamDeviceStatic()) {
                minOf(requestedMaxSize, 20) /* low ram max log size*/
            } else {
                requestedMaxSize
            }
        }
    }
}
+64 −0
Original line number Diff line number Diff line
/*
 * 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.util.kotlin.pairwiseBy
import kotlinx.coroutines.flow.Flow

/**
 * An interface that enables logging the difference between values in table format.
 *
 * Many objects that we want to log are data-y objects with a collection of fields. When logging
 * these objects, we want to log each field separately. This allows ABT (Android Bug Tool) to easily
 * highlight changes in individual fields.
 *
 * See [TableLogBuffer].
 */
interface Diffable<T> {
    /**
     * Finds the differences between [prevVal] and [this] and logs those diffs to [row].
     *
     * Each implementer should determine which individual fields have changed between [prevVal] and
     * [this], and only log the fields that have actually changed. This helps save buffer space.
     *
     * For example, if:
     * - prevVal = Object(val1=100, val2=200, val3=300)
     * - this = Object(val1=100, val2=200, val3=333)
     *
     * Then only the val3 change should be logged.
     */
    fun logDiffs(prevVal: T, row: TableRowLogger)
}

/**
 * Each time the flow is updated with a new value, logs the differences between the previous value
 * and the new value to the given [tableLogBuffer].
 *
 * The new value's [Diffable.logDiffs] method will be used to log the differences to the table.
 *
 * @param columnPrefix a prefix that will be applied to every column name that gets logged.
 */
fun <T : Diffable<T>> Flow<T>.logDiffsForTable(
    tableLogBuffer: TableLogBuffer,
    columnPrefix: String,
    initialValue: T,
): Flow<T> {
    return this.pairwiseBy(initialValue) { prevVal, newVal ->
        tableLogBuffer.logDiffs(columnPrefix, prevVal, newVal)
        newVal
    }
}
+90 −0
Original line number Diff line number Diff line
/*
 * 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

/**
 * A object used with [TableLogBuffer] to store changes in variables over time. Is recyclable.
 *
 * Each message represents a change to exactly 1 type, specified by [DataType].
 */
data class TableChange(
    var timestamp: Long = 0,
    var columnPrefix: String = "",
    var columnName: String = "",
    var type: DataType = DataType.EMPTY,
    var bool: Boolean = false,
    var int: Int = 0,
    var str: String? = null,
) {
    /** Resets to default values so that the object can be recycled. */
    fun reset(timestamp: Long, columnPrefix: String, columnName: String) {
        this.timestamp = timestamp
        this.columnPrefix = columnPrefix
        this.columnName = columnName
        this.type = DataType.EMPTY
        this.bool = false
        this.int = 0
        this.str = null
    }

    /** Sets this to store a string change. */
    fun set(value: String?) {
        type = DataType.STRING
        str = value
    }

    /** Sets this to store a boolean change. */
    fun set(value: Boolean) {
        type = DataType.BOOLEAN
        bool = value
    }

    /** Sets this to store an int change. */
    fun set(value: Int) {
        type = DataType.INT
        int = value
    }

    /** Returns true if this object has a change. */
    fun hasData(): Boolean {
        return columnName.isNotBlank() && type != DataType.EMPTY
    }

    fun getName(): String {
        return if (columnPrefix.isNotBlank()) {
            "$columnPrefix.$columnName"
        } else {
            columnName
        }
    }

    fun getVal(): String {
        return when (type) {
            DataType.EMPTY -> null
            DataType.STRING -> str
            DataType.INT -> int
            DataType.BOOLEAN -> bool
        }.toString()
    }

    enum class DataType {
        STRING,
        BOOLEAN,
        INT,
        EMPTY,
    }
}
+264 −0
Original line number Diff line number Diff line
/*
 * 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 androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.dump.DumpManager
import com.android.systemui.dump.DumpsysTableLogger
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,
) {
    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.
     */
    @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)
    }

    // 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 {
        val tableChange = buffer.advance()
        tableChange.reset(timestamp, prefix, columnName)
        return tableChange
    }

    /**
     * Registers this buffer as dumpables in [dumpManager]. Must be called for the table to be
     * dumped.
     *
     * This will be automatically called in [TableLogBufferFactory.create].
     */
    fun registerDumpables(dumpManager: DumpManager) {
        dumpManager.registerNormalDumpable("$name-changes", changeDumpable)
        dumpManager.registerNormalDumpable("$name-table", tableDumpable)
    }

    private val changeDumpable = Dumpable { pw, _ -> dumpChanges(pw) }
    private val tableDumpable = Dumpable { pw, _ -> dumpTable(pw) }

    /** Dumps the list of [TableChange] objects. */
    @Synchronized
    @VisibleForTesting
    fun dumpChanges(pw: PrintWriter) {
        for (i in 0 until buffer.size) {
            buffer[i].dump(pw)
        }
    }

    /** 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(" ")
        pw.print(this.getName())
        pw.print("=")
        pw.print(this.getVal())
        pw.println()
    }

    /**
     * Coalesces all the [TableChange] objects into a table of values of time and dumps the table.
     */
    // TODO(b/259454430): Since this is an expensive process, it could cause the bug report dump to
    //   fail and/or not dump anything else. We should move this processing to ABT (Android Bug
    //   Tool), where we have unlimited time to process.
    @Synchronized
    @VisibleForTesting
    fun dumpTable(pw: PrintWriter) {
        val messages = buffer.iterator().asSequence().toList()

        if (messages.isEmpty()) {
            return
        }

        // Step 1: Create list of column headers
        val headerSet = mutableSetOf<String>()
        messages.forEach { headerSet.add(it.getName()) }
        val headers: MutableList<String> = headerSet.toList().sorted().toMutableList()
        headers.add(0, "timestamp")

        // Step 2: Create a list with the current values for each column. Will be updated with each
        // change.
        val currentRow: MutableList<String> = MutableList(headers.size) { DEFAULT_COLUMN_VALUE }

        // Step 3: For each message, make the correct update to [currentRow] and save it to [rows].
        val columnIndices: Map<String, Int> =
            headers.mapIndexed { index, headerName -> headerName to index }.toMap()
        val allRows = mutableListOf<List<String>>()

        messages.forEach {
            if (!it.hasData()) {
                return@forEach
            }

            val formattedTimestamp = TABLE_LOG_DATE_FORMAT.format(it.timestamp)
            if (formattedTimestamp != currentRow[0]) {
                // The timestamp has updated, so save the previous row and continue to the next row
                allRows.add(currentRow.toList())
                currentRow[0] = formattedTimestamp
            }
            val columnIndex = columnIndices[it.getName()]!!
            currentRow[columnIndex] = it.getVal()
        }
        // Add the last row
        allRows.add(currentRow.toList())

        // Step 4: Dump the rows
        DumpsysTableLogger(
                name,
                headers,
                allRows,
            )
            .printTableData(pw)
    }

    /**
     * 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 DEFAULT_COLUMN_VALUE = "UNKNOWN"
Loading