Merge changes from topic "cherrypicker-L19100000960963656:N27300001373608395" into udc-qpr-dev

* changes:
  Move TableLogBuffer into its own DumpManager collection
  Rearchitect DumpManager to be registry-only
This commit is contained in:
Evan Laird
2023-06-02 21:55:43 +00:00
committed by Android (Google) Code Review
9 changed files with 770 additions and 600 deletions

View File

@@ -30,8 +30,10 @@ import com.android.internal.os.BinderInternal;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpHandler;
import com.android.systemui.dump.LogBufferEulogizer;
import com.android.systemui.dump.LogBufferFreezer;
import com.android.systemui.dump.SystemUIAuxiliaryDumpService;
import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager;
import com.android.systemui.statusbar.policy.BatteryStateNotifier;
import java.io.FileDescriptor;
@@ -44,22 +46,29 @@ public class SystemUIService extends Service {
private final Handler mMainHandler;
private final DumpHandler mDumpHandler;
private final BroadcastDispatcher mBroadcastDispatcher;
private final LogBufferEulogizer mLogBufferEulogizer;
private final LogBufferFreezer mLogBufferFreezer;
private final BatteryStateNotifier mBatteryStateNotifier;
private final UncaughtExceptionPreHandlerManager mUncaughtExceptionPreHandlerManager;
@Inject
public SystemUIService(
@Main Handler mainHandler,
DumpHandler dumpHandler,
BroadcastDispatcher broadcastDispatcher,
LogBufferEulogizer logBufferEulogizer,
LogBufferFreezer logBufferFreezer,
BatteryStateNotifier batteryStateNotifier) {
BatteryStateNotifier batteryStateNotifier,
UncaughtExceptionPreHandlerManager uncaughtExceptionPreHandlerManager) {
super();
mMainHandler = mainHandler;
mDumpHandler = dumpHandler;
mBroadcastDispatcher = broadcastDispatcher;
mLogBufferEulogizer = logBufferEulogizer;
mLogBufferFreezer = logBufferFreezer;
mBatteryStateNotifier = batteryStateNotifier;
mUncaughtExceptionPreHandlerManager = uncaughtExceptionPreHandlerManager;
}
@Override
@@ -71,7 +80,13 @@ public class SystemUIService extends Service {
// Finish initializing dump logic
mLogBufferFreezer.attach(mBroadcastDispatcher);
mDumpHandler.init();
// Attempt to dump all LogBuffers for any uncaught exception
mUncaughtExceptionPreHandlerManager.registerHandler((thread, throwable) -> {
if (throwable instanceof Exception) {
mLogBufferEulogizer.record(((Exception) throwable));
}
});
// If configured, set up a battery notification
if (getResources().getBoolean(R.bool.config_showNotificationForUnknownBatteryState)) {

View File

@@ -20,12 +20,16 @@ import android.content.Context
import android.os.SystemClock
import android.os.Trace
import com.android.systemui.CoreStartable
import com.android.systemui.ProtoDumpable
import com.android.systemui.R
import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL
import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL
import com.android.systemui.dump.DumpsysEntry.DumpableEntry
import com.android.systemui.dump.DumpsysEntry.LogBufferEntry
import com.android.systemui.dump.DumpsysEntry.TableLogBufferEntry
import com.android.systemui.dump.nano.SystemUIProtoDump
import com.android.systemui.log.LogBuffer
import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
import com.android.systemui.log.table.TableLogBuffer
import com.google.protobuf.nano.MessageNano
import java.io.BufferedOutputStream
import java.io.FileDescriptor
@@ -39,11 +43,11 @@ import javax.inject.Provider
*
* Dump output is split into two sections, CRITICAL and NORMAL. In general, the CRITICAL section
* contains all dumpables that were registered to the [DumpManager], while the NORMAL sections
* contains all [LogBuffer]s (due to their length).
* contains all [LogBuffer]s and [TableLogBuffer]s (due to their length).
*
* The CRITICAL and NORMAL sections can be found within a bug report by searching for
* "SERVICE com.android.systemui/.SystemUIService" and
* "SERVICE com.android.systemui/.dump.SystemUIAuxiliaryDumpService", respectively.
* The CRITICAL and NORMAL sections can be found within a bug report by searching for "SERVICE
* com.android.systemui/.SystemUIService" and "SERVICE
* com.android.systemui/.dump.SystemUIAuxiliaryDumpService", respectively.
*
* Finally, some or all of the dump can be triggered on-demand via adb (see below).
*
@@ -72,6 +76,7 @@ import javax.inject.Provider
* # To dump all dumpables or all buffers:
* $ <invocation> dumpables
* $ <invocation> buffers
* $ <invocation> tables
*
* # Finally, the following will simulate what we dump during the CRITICAL and NORMAL sections of a
* # bug report:
@@ -83,37 +88,26 @@ import javax.inject.Provider
* $ <invocation> --help
* ```
*/
class DumpHandler @Inject constructor(
class DumpHandler
@Inject
constructor(
private val context: Context,
private val dumpManager: DumpManager,
private val logBufferEulogizer: LogBufferEulogizer,
private val startables: MutableMap<Class<*>, Provider<CoreStartable>>,
private val uncaughtExceptionPreHandlerManager: UncaughtExceptionPreHandlerManager
) {
/**
* Registers an uncaught exception handler
*/
fun init() {
uncaughtExceptionPreHandlerManager.registerHandler { _, e ->
if (e is Exception) {
logBufferEulogizer.record(e)
}
}
}
/**
* Dump the diagnostics! Behavior can be controlled via [args].
*/
/** Dump the diagnostics! Behavior can be controlled via [args]. */
fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
Trace.beginSection("DumpManager#dump()")
val start = SystemClock.uptimeMillis()
val parsedArgs = try {
parseArgs(args)
} catch (e: ArgParseException) {
pw.println(e.message)
return
}
val parsedArgs =
try {
parseArgs(args)
} catch (e: ArgParseException) {
pw.println(e.message)
return
}
when {
parsedArgs.dumpPriority == PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs)
@@ -134,6 +128,7 @@ class DumpHandler @Inject constructor(
"bugreport-normal" -> dumpNormal(pw, args)
"dumpables" -> dumpDumpables(pw, args)
"buffers" -> dumpBuffers(pw, args)
"tables" -> dumpTables(pw, args)
"config" -> dumpConfig(pw)
"help" -> dumpHelp(pw)
else -> {
@@ -147,44 +142,65 @@ class DumpHandler @Inject constructor(
}
private fun dumpCritical(pw: PrintWriter, args: ParsedArgs) {
dumpManager.dumpCritical(pw, args.rawArgs)
val targets = dumpManager.getDumpables()
for (target in targets) {
if (target.priority == DumpPriority.CRITICAL) {
dumpDumpable(target, pw, args.rawArgs)
}
}
dumpConfig(pw)
}
private fun dumpNormal(pw: PrintWriter, args: ParsedArgs) {
dumpManager.dumpNormal(pw, args.rawArgs, args.tailLength)
val targets = dumpManager.getDumpables()
for (target in targets) {
if (target.priority == DumpPriority.NORMAL) {
dumpDumpable(target, pw, args.rawArgs)
}
}
val buffers = dumpManager.getLogBuffers()
for (buffer in buffers) {
dumpBuffer(buffer, pw, args.tailLength)
}
val tableBuffers = dumpManager.getTableLogBuffers()
for (table in tableBuffers) {
dumpTableBuffer(table, pw, args.rawArgs)
}
logBufferEulogizer.readEulogyIfPresent(pw)
}
private fun dumpDumpables(pw: PrintWriter, args: ParsedArgs) {
if (args.listOnly) {
dumpManager.listDumpables(pw)
} else {
dumpManager.dumpDumpables(pw, args.rawArgs)
private fun dumpDumpables(pw: PrintWriter, args: ParsedArgs) =
dumpManager.getDumpables().listOrDumpEntries(pw, args)
private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) =
dumpManager.getLogBuffers().listOrDumpEntries(pw, args)
private fun dumpTables(pw: PrintWriter, args: ParsedArgs) =
dumpManager.getTableLogBuffers().listOrDumpEntries(pw, args)
private fun listTargetNames(targets: Collection<DumpsysEntry>, pw: PrintWriter) {
for (target in targets) {
pw.println(target.name)
}
}
private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) {
if (args.listOnly) {
dumpManager.listBuffers(pw)
} else {
dumpManager.dumpBuffers(pw, args.tailLength)
}
}
private fun dumpProtoTargets(
targets: List<String>,
fd: FileDescriptor,
args: ParsedArgs
) {
private fun dumpProtoTargets(targets: List<String>, fd: FileDescriptor, args: ParsedArgs) {
val systemUIProto = SystemUIProtoDump()
val dumpables = dumpManager.getDumpables()
if (targets.isNotEmpty()) {
for (target in targets) {
dumpManager.dumpProtoTarget(target, systemUIProto, args.rawArgs)
findBestProtoTargetMatch(dumpables, target)?.dumpProto(systemUIProto, args.rawArgs)
}
} else {
dumpManager.dumpProtoDumpables(systemUIProto, args.rawArgs)
// Dump all protos
for (dumpable in dumpables) {
(dumpable.dumpable as? ProtoDumpable)?.dumpProto(systemUIProto, args.rawArgs)
}
}
val buffer = BufferedOutputStream(FileOutputStream(fd))
buffer.use {
it.write(MessageNano.toByteArray(systemUIProto))
@@ -192,36 +208,70 @@ class DumpHandler @Inject constructor(
}
}
private fun dumpTargets(
targets: List<String>,
pw: PrintWriter,
args: ParsedArgs
) {
// Attempts to dump the target list to the given PrintWriter. Since the arguments come in as
// a list of strings, we use the [findBestTargetMatch] method to determine the most-correct
// target with the given search string.
private fun dumpTargets(targets: List<String>, pw: PrintWriter, args: ParsedArgs) {
if (targets.isNotEmpty()) {
for (target in targets) {
dumpManager.dumpTarget(target, pw, args.rawArgs, args.tailLength)
val dumpables = dumpManager.getDumpables()
val buffers = dumpManager.getLogBuffers()
val tableBuffers = dumpManager.getTableLogBuffers()
targets.forEach { target ->
findTargetInCollection(target, dumpables, buffers, tableBuffers)?.dump(pw, args)
}
} else {
if (args.listOnly) {
val dumpables = dumpManager.getDumpables()
val buffers = dumpManager.getLogBuffers()
pw.println("Dumpables:")
dumpManager.listDumpables(pw)
listTargetNames(dumpables, pw)
pw.println()
pw.println("Buffers:")
dumpManager.listBuffers(pw)
listTargetNames(buffers, pw)
} else {
pw.println("Nothing to dump :(")
}
}
}
private fun findTargetInCollection(
target: String,
dumpables: Collection<DumpableEntry>,
logBuffers: Collection<LogBufferEntry>,
tableBuffers: Collection<TableLogBufferEntry>,
) =
sequence {
findBestTargetMatch(dumpables, target)?.let { yield(it) }
findBestTargetMatch(logBuffers, target)?.let { yield(it) }
findBestTargetMatch(tableBuffers, target)?.let { yield(it) }
}
.sortedBy { it.name }
.minByOrNull { it.name.length }
private fun dumpDumpable(entry: DumpableEntry, pw: PrintWriter, args: Array<String>) {
pw.preamble(entry)
entry.dumpable.dump(pw, args)
}
private fun dumpBuffer(entry: LogBufferEntry, pw: PrintWriter, tailLength: Int) {
pw.preamble(entry)
entry.buffer.dump(pw, tailLength)
}
private fun dumpTableBuffer(buffer: TableLogBufferEntry, pw: PrintWriter, args: Array<String>) {
pw.preamble(buffer)
buffer.table.dump(pw, args)
}
private fun dumpConfig(pw: PrintWriter) {
pw.println("SystemUiServiceComponents configuration:")
pw.print("vendor component: ")
pw.println(context.resources.getString(R.string.config_systemUIVendorServiceComponent))
val services: MutableList<String> = startables.keys
.map({ cls: Class<*> -> cls.simpleName })
.toMutableList()
val services: MutableList<String> =
startables.keys.map({ cls: Class<*> -> cls.simpleName }).toMutableList()
services.add(context.resources.getString(R.string.config_systemUIVendorServiceComponent))
dumpServiceList(pw, "global", services.toTypedArray())
@@ -265,6 +315,7 @@ class DumpHandler @Inject constructor(
pw.println("Special commands:")
pw.println("$ <invocation> dumpables")
pw.println("$ <invocation> buffers")
pw.println("$ <invocation> tables")
pw.println("$ <invocation> bugreport-critical")
pw.println("$ <invocation> bugreport-normal")
pw.println("$ <invocation> config")
@@ -274,6 +325,7 @@ class DumpHandler @Inject constructor(
pw.println("$ <invocation> --list")
pw.println("$ <invocation> dumpables --list")
pw.println("$ <invocation> buffers --list")
pw.println("$ <invocation> tables --list")
pw.println()
pw.println("Show only the most recent N lines of buffers")
@@ -291,24 +343,26 @@ class DumpHandler @Inject constructor(
iterator.remove()
when (arg) {
PRIORITY_ARG -> {
pArgs.dumpPriority = readArgument(iterator, PRIORITY_ARG) {
if (PRIORITY_OPTIONS.contains(it)) {
it
} else {
throw IllegalArgumentException()
pArgs.dumpPriority =
readArgument(iterator, PRIORITY_ARG) {
if (PRIORITY_OPTIONS.contains(it)) {
it
} else {
throw IllegalArgumentException()
}
}
}
}
PROTO -> pArgs.proto = true
"-t", "--tail" -> {
pArgs.tailLength = readArgument(iterator, arg) {
it.toInt()
}
"-t",
"--tail" -> {
pArgs.tailLength = readArgument(iterator, arg) { it.toInt() }
}
"-l", "--list" -> {
"-l",
"--list" -> {
pArgs.listOnly = true
}
"-h", "--help" -> {
"-h",
"--help" -> {
pArgs.command = "help"
}
// This flag is passed as part of the proto dump in Bug reports, we can ignore
@@ -345,29 +399,131 @@ class DumpHandler @Inject constructor(
}
}
private fun DumpsysEntry.dump(pw: PrintWriter, args: ParsedArgs) =
when (this) {
is DumpableEntry -> dumpDumpable(this, pw, args.rawArgs)
is LogBufferEntry -> dumpBuffer(this, pw, args.tailLength)
is TableLogBufferEntry -> dumpTableBuffer(this, pw, args.rawArgs)
}
private fun Collection<DumpsysEntry>.listOrDumpEntries(pw: PrintWriter, args: ParsedArgs) =
if (args.listOnly) {
listTargetNames(this, pw)
} else {
forEach { it.dump(pw, args) }
}
companion object {
const val PRIORITY_ARG = "--dump-priority"
const val PRIORITY_ARG_CRITICAL = "CRITICAL"
const val PRIORITY_ARG_NORMAL = "NORMAL"
const val PROTO = "--proto"
/**
* Important: do not change this divider without updating any bug report processing tools
* (e.g. ABT), since this divider is used to determine boundaries for bug report views
*/
const val DUMPSYS_DUMPABLE_DIVIDER =
"----------------------------------------------------------------------------"
/**
* Important: do not change this divider without updating any bug report processing tools
* (e.g. ABT), since this divider is used to determine boundaries for bug report views
*/
const val DUMPSYS_BUFFER_DIVIDER =
"============================================================================"
private fun findBestTargetMatch(c: Collection<DumpsysEntry>, target: String) =
c.asSequence().filter { it.name.endsWith(target) }.minByOrNull { it.name.length }
private fun findBestProtoTargetMatch(
c: Collection<DumpableEntry>,
target: String
): ProtoDumpable? =
c.asSequence()
.filter { it.name.endsWith(target) }
.filter { it.dumpable is ProtoDumpable }
.minByOrNull { it.name.length }
?.dumpable as? ProtoDumpable
private fun PrintWriter.preamble(entry: DumpsysEntry) =
when (entry) {
// Historically TableLogBuffer was not separate from dumpables, so they have the
// same header
is DumpableEntry,
is TableLogBufferEntry -> {
println()
println(entry.name)
println(DUMPSYS_DUMPABLE_DIVIDER)
}
is LogBufferEntry -> {
println()
println()
println("BUFFER ${entry.name}:")
println(DUMPSYS_BUFFER_DIVIDER)
}
}
/**
* Zero-arg utility to write a [DumpableEntry] to the given [PrintWriter] in a
* dumpsys-appropriate format.
*/
private fun dumpDumpable(entry: DumpableEntry, pw: PrintWriter) {
pw.preamble(entry)
entry.dumpable.dump(pw, arrayOf())
}
/**
* Zero-arg utility to write a [LogBufferEntry] to the given [PrintWriter] in a
* dumpsys-appropriate format.
*/
private fun dumpBuffer(entry: LogBufferEntry, pw: PrintWriter) {
pw.preamble(entry)
entry.buffer.dump(pw, 0)
}
/**
* Zero-arg utility to write a [TableLogBufferEntry] to the given [PrintWriter] in a
* dumpsys-appropriate format.
*/
private fun dumpTableBuffer(entry: TableLogBufferEntry, pw: PrintWriter) {
pw.preamble(entry)
entry.table.dump(pw, arrayOf())
}
/**
* Zero-arg utility to write a [DumpsysEntry] to the given [PrintWriter] in a
* dumpsys-appropriate format.
*/
fun DumpsysEntry.dump(pw: PrintWriter) {
when (this) {
is DumpableEntry -> dumpDumpable(this, pw)
is LogBufferEntry -> dumpBuffer(this, pw)
is TableLogBufferEntry -> dumpTableBuffer(this, pw)
}
}
/** Format [entries] in a dumpsys-appropriate way, using [pw] */
fun dumpEntries(entries: Collection<DumpsysEntry>, pw: PrintWriter) {
entries.forEach { it.dump(pw) }
}
}
}
private val PRIORITY_OPTIONS = arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_NORMAL)
private val COMMANDS = arrayOf(
private val COMMANDS =
arrayOf(
"bugreport-critical",
"bugreport-normal",
"buffers",
"dumpables",
"tables",
"config",
"help"
)
)
private class ParsedArgs(
val rawArgs: Array<String>,
val nonFlagArgs: List<String>
) {
private class ParsedArgs(val rawArgs: Array<String>, val nonFlagArgs: List<String>) {
var dumpPriority: String? = null
var tailLength: Int = 0
var command: String? = null

View File

@@ -18,9 +18,11 @@ package com.android.systemui.dump
import com.android.systemui.Dumpable
import com.android.systemui.ProtoDumpable
import com.android.systemui.dump.nano.SystemUIProtoDump
import com.android.systemui.dump.DumpsysEntry.DumpableEntry
import com.android.systemui.dump.DumpsysEntry.LogBufferEntry
import com.android.systemui.dump.DumpsysEntry.TableLogBufferEntry
import com.android.systemui.log.LogBuffer
import java.io.PrintWriter
import com.android.systemui.log.table.TableLogBuffer
import java.util.TreeMap
import javax.inject.Inject
import javax.inject.Singleton
@@ -37,8 +39,9 @@ import javax.inject.Singleton
@Singleton
open class DumpManager @Inject constructor() {
// NOTE: Using TreeMap ensures that iteration is in a predictable & alphabetical order.
private val dumpables: MutableMap<String, RegisteredDumpable<Dumpable>> = TreeMap()
private val buffers: MutableMap<String, RegisteredDumpable<LogBuffer>> = TreeMap()
private val dumpables: MutableMap<String, DumpableEntry> = TreeMap()
private val buffers: MutableMap<String, LogBufferEntry> = TreeMap()
private val tableLogBuffers: MutableMap<String, TableLogBufferEntry> = TreeMap()
/** See [registerCriticalDumpable]. */
fun registerCriticalDumpable(module: Dumpable) {
@@ -77,14 +80,14 @@ open class DumpManager @Inject constructor() {
* Register a dumpable to be called during a bug report.
*
* @param name The name to register the dumpable under. This is typically the qualified class
* name of the thing being dumped (getClass().getName()), but can be anything as long as it
* doesn't clash with an existing registration.
* name of the thing being dumped (getClass().getName()), but can be anything as long as it
* doesn't clash with an existing registration.
* @param priority the priority level of this dumpable, which affects at what point in the bug
* report this gets dump. By default, the dumpable will be called during the CRITICAL section of
* the bug report, so don't dump an excessive amount of stuff here.
* report this gets dump. By default, the dumpable will be called during the CRITICAL section
* of the bug report, so don't dump an excessive amount of stuff here.
*
* TODO(b/259973758): Replace all calls to this method with calls to [registerCriticalDumpable]
* or [registerNormalDumpable] instead.
* or [registerNormalDumpable] instead.
*/
@Synchronized
@JvmOverloads
@@ -98,7 +101,7 @@ open class DumpManager @Inject constructor() {
throw IllegalArgumentException("'$name' is already registered")
}
dumpables[name] = RegisteredDumpable(name, module, priority)
dumpables[name] = DumpableEntry(module, name, priority)
}
/**
@@ -110,217 +113,62 @@ open class DumpManager @Inject constructor() {
registerDumpable(module::class.java.simpleName, module)
}
/**
* Unregisters a previously-registered dumpable.
*/
/** Unregisters a previously-registered dumpable. */
@Synchronized
fun unregisterDumpable(name: String) {
dumpables.remove(name)
}
/**
* Register a [LogBuffer] to be dumped during a bug report.
*/
/** Register a [LogBuffer] to be dumped during a bug report. */
@Synchronized
fun registerBuffer(name: String, buffer: LogBuffer) {
if (!canAssignToNameLocked(name, buffer)) {
throw IllegalArgumentException("'$name' is already registered")
}
buffers[name] = LogBufferEntry(buffer, name)
}
/** Register a [TableLogBuffer] to be dumped during a bugreport */
@Synchronized
fun registerTableLogBuffer(name: String, buffer: TableLogBuffer) {
if (!canAssignToNameLocked(name, buffer)) {
throw IllegalArgumentException("'$name' is already registered")
}
// All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of
// data.
buffers[name] = RegisteredDumpable(name, buffer, DumpPriority.NORMAL)
tableLogBuffers[name] = TableLogBufferEntry(buffer, name)
}
/**
* Dumps the alphabetically first, shortest-named dumpable or buffer whose registered name ends
* with [target].
*/
@Synchronized
fun dumpTarget(
target: String,
pw: PrintWriter,
args: Array<String>,
tailLength: Int,
) {
sequence {
findBestTargetMatch(dumpables, target)?.let {
yield(it.name to { dumpDumpable(it, pw, args) })
}
findBestTargetMatch(buffers, target)?.let {
yield(it.name to { dumpBuffer(it, pw, tailLength) })
}
}.sortedBy { it.first }.minByOrNull { it.first.length }?.second?.invoke()
}
@Synchronized fun getDumpables(): Collection<DumpableEntry> = dumpables.values.toList()
@Synchronized fun getLogBuffers(): Collection<LogBufferEntry> = buffers.values.toList()
@Synchronized
fun dumpProtoTarget(
target: String,
protoDump: SystemUIProtoDump,
args: Array<String>
) {
findBestProtoTargetMatch(dumpables, target)?.let {
dumpProtoDumpable(it, protoDump, args)
}
}
@Synchronized
fun dumpProtoDumpables(
systemUIProtoDump: SystemUIProtoDump,
args: Array<String>
) {
for (dumpable in dumpables.values) {
if (dumpable.dumpable is ProtoDumpable) {
dumpProtoDumpable(
dumpable.dumpable,
systemUIProtoDump,
args
)
}
}
}
/**
* Dumps all registered dumpables with critical priority to [pw]
*/
@Synchronized
fun dumpCritical(pw: PrintWriter, args: Array<String>) {
for (dumpable in dumpables.values) {
if (dumpable.priority == DumpPriority.CRITICAL) {
dumpDumpable(dumpable, pw, args)
}
}
}
/**
* To [pw], dumps (1) all registered dumpables with normal priority; and (2) all [LogBuffer]s.
*/
@Synchronized
fun dumpNormal(pw: PrintWriter, args: Array<String>, tailLength: Int = 0) {
for (dumpable in dumpables.values) {
if (dumpable.priority == DumpPriority.NORMAL) {
dumpDumpable(dumpable, pw, args)
}
}
for (buffer in buffers.values) {
dumpBuffer(buffer, pw, tailLength)
}
}
/**
* Dump all the instances of [Dumpable].
*/
@Synchronized
fun dumpDumpables(pw: PrintWriter, args: Array<String>) {
for (module in dumpables.values) {
dumpDumpable(module, pw, args)
}
}
/**
* Dumps the names of all registered dumpables (one per line)
*/
@Synchronized
fun listDumpables(pw: PrintWriter) {
for (module in dumpables.values) {
pw.println(module.name)
}
}
/**
* Dumps all registered [LogBuffer]s to [pw]
*/
@Synchronized
fun dumpBuffers(pw: PrintWriter, tailLength: Int) {
for (buffer in buffers.values) {
dumpBuffer(buffer, pw, tailLength)
}
}
/**
* Dumps the names of all registered buffers (one per line)
*/
@Synchronized
fun listBuffers(pw: PrintWriter) {
for (buffer in buffers.values) {
pw.println(buffer.name)
}
}
fun getTableLogBuffers(): Collection<TableLogBufferEntry> = tableLogBuffers.values.toList()
@Synchronized
fun freezeBuffers() {
for (buffer in buffers.values) {
buffer.dumpable.freeze()
buffer.buffer.freeze()
}
}
@Synchronized
fun unfreezeBuffers() {
for (buffer in buffers.values) {
buffer.dumpable.unfreeze()
buffer.buffer.unfreeze()
}
}
private fun dumpDumpable(
dumpable: RegisteredDumpable<Dumpable>,
pw: PrintWriter,
args: Array<String>
) {
pw.println()
pw.println("${dumpable.name}:")
pw.println("----------------------------------------------------------------------------")
dumpable.dumpable.dump(pw, args)
}
private fun dumpBuffer(
buffer: RegisteredDumpable<LogBuffer>,
pw: PrintWriter,
tailLength: Int
) {
pw.println()
pw.println()
pw.println("BUFFER ${buffer.name}:")
pw.println("============================================================================")
buffer.dumpable.dump(pw, tailLength)
}
private fun dumpProtoDumpable(
protoDumpable: ProtoDumpable,
systemUIProtoDump: SystemUIProtoDump,
args: Array<String>
) {
protoDumpable.dumpProto(systemUIProtoDump, args)
}
private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean {
val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable
val existingDumpable =
dumpables[name]?.dumpable ?: buffers[name]?.buffer ?: tableLogBuffers[name]?.table
return existingDumpable == null || newDumpable == existingDumpable
}
private fun <V : Any> findBestTargetMatch(map: Map<String, V>, target: String): V? = map
.asSequence()
.filter { it.key.endsWith(target) }
.minByOrNull { it.key.length }
?.value
private fun findBestProtoTargetMatch(
map: Map<String, RegisteredDumpable<Dumpable>>,
target: String
): ProtoDumpable? = map
.asSequence()
.filter { it.key.endsWith(target) }
.filter { it.value.dumpable is ProtoDumpable }
.minByOrNull { it.key.length }
?.value?.dumpable as? ProtoDumpable
}
private data class RegisteredDumpable<T>(
val name: String,
val dumpable: T,
val priority: DumpPriority,
)
/**
* The priority level for a given dumpable, which affects at what point in the bug report this gets
* dumped.

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2023 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 com.android.systemui.Dumpable
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.table.TableLogBuffer
/**
* A DumpsysEntry is a named, registered entry tracked by [DumpManager] which can be addressed and
* used both in a bugreport / dumpsys invocation or in an individual CLI implementation.
*
* The idea here is that we define every type that [DumpManager] knows about and defines the minimum
* shared interface between each type. So far, just [name] and [priority]. This way, [DumpManager]
* can just store them in separate maps and do the minimal amount of work to discriminate between
* them.
*
* Individual consumers can request these participants in a list via the relevant get* methods on
* [DumpManager]
*/
sealed interface DumpsysEntry {
val name: String
val priority: DumpPriority
data class DumpableEntry(
val dumpable: Dumpable,
override val name: String,
override val priority: DumpPriority,
) : DumpsysEntry
data class LogBufferEntry(
val buffer: LogBuffer,
override val name: String,
) : DumpsysEntry {
// All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of
// data.
override val priority: DumpPriority = DumpPriority.NORMAL
}
data class TableLogBufferEntry(
val table: TableLogBuffer,
override val name: String,
) : DumpsysEntry {
// All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of
// data.
override val priority: DumpPriority = DumpPriority.NORMAL
}
}

View File

@@ -20,6 +20,7 @@ import android.content.Context
import android.icu.text.SimpleDateFormat
import android.util.Log
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpHandler.Companion.dumpEntries
import com.android.systemui.log.LogBuffer
import com.android.systemui.util.io.Files
import com.android.systemui.util.time.SystemClock
@@ -48,20 +49,21 @@ class LogBufferEulogizer(
private val files: Files,
private val logPath: Path,
private val minWriteGap: Long,
private val maxLogAgeToDump: Long
private val maxLogAgeToDump: Long,
) {
@Inject constructor(
@Inject
constructor(
context: Context,
dumpManager: DumpManager,
systemClock: SystemClock,
files: Files
files: Files,
) : this(
dumpManager,
systemClock,
files,
Paths.get(context.filesDir.toPath().toString(), "log_buffers.txt"),
MIN_WRITE_GAP,
MAX_AGE_TO_DUMP
MAX_AGE_TO_DUMP,
)
/**
@@ -91,7 +93,8 @@ class LogBufferEulogizer(
pw.println()
pw.println("Dump triggered by exception:")
reason.printStackTrace(pw)
dumpManager.dumpBuffers(pw, 0)
val buffers = dumpManager.getLogBuffers()
dumpEntries(buffers, pw)
duration = systemClock.uptimeMillis() - start
pw.println()
pw.println("Buffer eulogy took ${duration}ms")
@@ -105,16 +108,17 @@ class LogBufferEulogizer(
return reason
}
/**
* If a eulogy file is present, writes its contents to [pw].
*/
/** If a eulogy file is present, writes its contents to [pw]. */
fun readEulogyIfPresent(pw: PrintWriter) {
try {
val millisSinceLastWrite = getMillisSinceLastWrite(logPath)
if (millisSinceLastWrite > maxLogAgeToDump) {
Log.i(TAG, "Not eulogizing buffers; they are " +
Log.i(
TAG,
"Not eulogizing buffers; they are " +
TimeUnit.HOURS.convert(millisSinceLastWrite, TimeUnit.MILLISECONDS) +
" hours old")
" hours old"
)
return
}
@@ -122,9 +126,7 @@ class LogBufferEulogizer(
pw.println()
pw.println()
pw.println("=============== BUFFERS FROM MOST RECENT CRASH ===============")
s.forEach { line ->
pw.println(line)
}
s.forEach { line -> pw.println(line) }
}
} catch (e: IOException) {
// File doesn't exist, okay
@@ -134,12 +136,13 @@ class LogBufferEulogizer(
}
private fun getMillisSinceLastWrite(path: Path): Long {
val stats = try {
files.readAttributes(path, BasicFileAttributes::class.java)
} catch (e: IOException) {
// File doesn't exist
null
}
val stats =
try {
files.readAttributes(path, BasicFileAttributes::class.java)
} catch (e: IOException) {
// File doesn't exist
null
}
return systemClock.currentTimeMillis() - (stats?.lastModifiedTime()?.toMillis() ?: 0)
}
}
@@ -147,4 +150,4 @@ class LogBufferEulogizer(
private const val TAG = "BufferEulogizer"
private val MIN_WRITE_GAP = TimeUnit.MINUTES.toMillis(5)
private val MAX_AGE_TO_DUMP = TimeUnit.HOURS.toMillis(48)
private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)

View File

@@ -61,7 +61,7 @@ constructor(
bgDispatcher,
coroutineScope,
)
dumpManager.registerNormalDumpable(name, tableBuffer)
dumpManager.registerTableLogBuffer(name, tableBuffer)
tableBuffer.init()
return tableBuffer
}

View File

@@ -22,7 +22,7 @@ import com.android.systemui.Dumpable
import com.android.systemui.ProtoDumpable
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.LogBuffer
import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.google.common.truth.Truth.assertThat
@@ -45,8 +45,6 @@ class DumpHandlerTest : SysuiTestCase() {
@Mock
private lateinit var logBufferEulogizer: LogBufferEulogizer
@Mock
private lateinit var exceptionHandlerManager: UncaughtExceptionPreHandlerManager
@Mock
private lateinit var pw: PrintWriter
@@ -70,6 +68,11 @@ class DumpHandlerTest : SysuiTestCase() {
@Mock
private lateinit var buffer2: LogBuffer
@Mock
private lateinit var table1: TableLogBuffer
@Mock
private lateinit var table2: TableLogBuffer
private val dumpManager = DumpManager()
@Before
@@ -83,21 +86,22 @@ class DumpHandlerTest : SysuiTestCase() {
mutableMapOf(
EmptyCoreStartable::class.java to Provider { EmptyCoreStartable() }
),
exceptionHandlerManager
)
}
@Test
fun testDumpablesCanBeDumpedSelectively() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerDumpable("dumpable1", dumpable1)
dumpManager.registerDumpable("dumpable2", dumpable2)
dumpManager.registerDumpable("dumpable3", dumpable3)
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
dumpManager.registerTableLogBuffer("table1", table1)
dumpManager.registerTableLogBuffer("table2", table2)
// WHEN some of them are dumped explicitly
val args = arrayOf("dumpable1", "dumpable3", "buffer2")
val args = arrayOf("dumpable1", "dumpable3", "buffer2", "table2")
dumpHandler.dump(fd, pw, args)
// THEN only the requested ones have their dump() method called
@@ -108,12 +112,14 @@ class DumpHandlerTest : SysuiTestCase() {
verify(dumpable3).dump(pw, args)
verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt())
verify(buffer2).dump(pw, 0)
verify(table1, never()).dump(any(), any())
verify(table2).dump(pw, args)
}
@Test
fun testDumpableMatchingIsBasedOnEndOfTag() {
// GIVEN a dumpable registered to the manager
dumpManager.registerDumpable("com.android.foo.bar.dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("com.android.foo.bar.dumpable1", dumpable1)
// WHEN that module is dumped
val args = arrayOf("dumpable1")
@@ -131,6 +137,8 @@ class DumpHandlerTest : SysuiTestCase() {
dumpManager.registerNormalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
dumpManager.registerTableLogBuffer("table1", table1)
dumpManager.registerTableLogBuffer("table2", table2)
// WHEN a critical dump is requested
val args = arrayOf("--dump-priority", "CRITICAL")
@@ -144,6 +152,8 @@ class DumpHandlerTest : SysuiTestCase() {
any(Array<String>::class.java))
verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt())
verify(buffer2, never()).dump(any(PrintWriter::class.java), anyInt())
verify(table1, never()).dump(any(), any())
verify(table2, never()).dump(any(), any())
}
@Test
@@ -154,6 +164,8 @@ class DumpHandlerTest : SysuiTestCase() {
dumpManager.registerNormalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
dumpManager.registerTableLogBuffer("table1", table1)
dumpManager.registerTableLogBuffer("table2", table2)
// WHEN a normal dump is requested
val args = arrayOf("--dump-priority", "NORMAL")
@@ -169,6 +181,8 @@ class DumpHandlerTest : SysuiTestCase() {
verify(dumpable3).dump(pw, args)
verify(buffer1).dump(pw, 0)
verify(buffer2).dump(pw, 0)
verify(table1).dump(pw, args)
verify(table2).dump(pw, args)
}
@Test
@@ -183,6 +197,81 @@ class DumpHandlerTest : SysuiTestCase() {
assertThat(stringWriter.toString()).contains(EmptyCoreStartable::class.java.simpleName)
}
@Test
fun testDumpBuffers() {
// GIVEN a variety of registered dumpables and buffers and tables
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerNormalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
dumpManager.registerTableLogBuffer("table1", table1)
dumpManager.registerTableLogBuffer("table2", table2)
// WHEN a buffer dump is requested
val args = arrayOf("buffers", "--tail", "1")
dumpHandler.dump(fd, pw, args)
// THEN all buffers are dumped (and no dumpables or tables)
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2, never()).dump(any(), any())
verify(dumpable3, never()).dump(any(), any())
verify(buffer1).dump(pw, tailLength = 1)
verify(buffer2).dump(pw, tailLength = 1)
verify(table1, never()).dump(any(), any())
verify(table2, never()).dump(any(), any())
}
@Test
fun testDumpDumpables() {
// GIVEN a variety of registered dumpables and buffers and tables
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerNormalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
dumpManager.registerTableLogBuffer("table1", table1)
dumpManager.registerTableLogBuffer("table2", table2)
// WHEN a dumpable dump is requested
val args = arrayOf("dumpables")
dumpHandler.dump(fd, pw, args)
// THEN all dumpables are dumped (both critical and normal) (and no dumpables)
verify(dumpable1).dump(pw, args)
verify(dumpable2).dump(pw, args)
verify(dumpable3).dump(pw, args)
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2, never()).dump(any(), anyInt())
verify(table1, never()).dump(any(), any())
verify(table2, never()).dump(any(), any())
}
@Test
fun testDumpTables() {
// GIVEN a variety of registered dumpables and buffers and tables
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerNormalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
dumpManager.registerTableLogBuffer("table1", table1)
dumpManager.registerTableLogBuffer("table2", table2)
// WHEN a dumpable dump is requested
val args = arrayOf("tables")
dumpHandler.dump(fd, pw, args)
// THEN all dumpables are dumped (both critical and normal) (and no dumpables)
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2, never()).dump(any(), any())
verify(dumpable3, never()).dump(any(), any())
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2, never()).dump(any(), anyInt())
verify(table1).dump(pw, args)
verify(table2).dump(pw, args)
}
@Test
fun testDumpAllProtoDumpables() {
dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
@@ -207,6 +296,123 @@ class DumpHandlerTest : SysuiTestCase() {
verify(protoDumpable2, never()).dumpProto(any(), any())
}
@Test
fun testDumpTarget_selectsShortestNamedDumpable() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("first-dumpable", dumpable1)
dumpManager.registerCriticalDumpable("scnd-dumpable", dumpable2)
dumpManager.registerCriticalDumpable("third-dumpable", dumpable3)
// WHEN a dumpable is dumped by a suffix that matches multiple options
val args = arrayOf("dumpable")
dumpHandler.dump(fd, pw, args)
// THEN the matching dumpable with the shorter name is dumped
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2).dump(pw, args)
verify(dumpable3, never()).dump(any(), any())
}
@Test
fun testDumpTarget_selectsShortestNamedBuffer() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerBuffer("first-buffer", buffer1)
dumpManager.registerBuffer("scnd-buffer", buffer2)
// WHEN a dumpable is dumped by a suffix that matches multiple options
val args = arrayOf("buffer", "--tail", "14")
dumpHandler.dump(fd, pw, args)
// THEN the matching buffer with the shorter name is dumped
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2).dump(pw, tailLength = 14)
}
@Test
fun testDumpTarget_selectsShortestNamedMatch_dumpable() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("big-buffer1", buffer1)
dumpManager.registerBuffer("big-buffer2", buffer2)
// WHEN a dumpable is dumped by a suffix that matches multiple options
val args = arrayOf("2")
dumpHandler.dump(fd, pw, args)
// THEN the matching dumpable with the shorter name is dumped
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2).dump(pw, args)
verify(dumpable3, never()).dump(any(), any())
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2, never()).dump(any(), anyInt())
}
@Test
fun testDumpTarget_selectsShortestNamedMatch_buffer() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
// WHEN a dumpable is dumped by a suffix that matches multiple options
val args = arrayOf("2", "--tail", "14")
dumpHandler.dump(fd, pw, args)
// THEN the matching buffer with the shorter name is dumped
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2, never()).dump(any(), any())
verify(dumpable3, never()).dump(any(), any())
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2).dump(pw, tailLength = 14)
}
@Test
fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_dumpable() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("d1x", dumpable1)
dumpManager.registerCriticalDumpable("d2x", dumpable2)
dumpManager.registerCriticalDumpable("a3x", dumpable3)
dumpManager.registerBuffer("ab1x", buffer1)
dumpManager.registerBuffer("b2x", buffer2)
// WHEN a dumpable is dumped by a suffix that matches multiple options
val args = arrayOf("x")
dumpHandler.dump(fd, pw, args)
// THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2, never()).dump(any(), any())
verify(dumpable3).dump(pw, args)
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2, never()).dump(any(), anyInt())
}
@Test
fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_buffer() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("d1x", dumpable1)
dumpManager.registerCriticalDumpable("d2x", dumpable2)
dumpManager.registerCriticalDumpable("az1x", dumpable3)
dumpManager.registerBuffer("b1x", buffer1)
dumpManager.registerBuffer("b2x", buffer2)
// WHEN a dumpable is dumped by a suffix that matches multiple options
val args = arrayOf("x", "--tail", "14")
dumpHandler.dump(fd, pw, args)
// THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2, never()).dump(any(), any())
verify(dumpable3, never()).dump(any(), any())
verify(buffer1).dump(pw, tailLength = 14)
verify(buffer2, never()).dump(any(), anyInt())
}
private class EmptyCoreStartable : CoreStartable {
override fun start() {}
}

View File

@@ -20,21 +20,17 @@ import androidx.test.filters.SmallTest
import com.android.systemui.Dumpable
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.LogBuffer
import com.android.systemui.util.mockito.any
import java.io.PrintWriter
import com.android.systemui.log.table.TableLogBuffer
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
class DumpManagerTest : SysuiTestCase() {
@Mock private lateinit var pw: PrintWriter
@Mock private lateinit var dumpable1: Dumpable
@Mock private lateinit var dumpable2: Dumpable
@Mock private lateinit var dumpable3: Dumpable
@@ -42,6 +38,9 @@ class DumpManagerTest : SysuiTestCase() {
@Mock private lateinit var buffer1: LogBuffer
@Mock private lateinit var buffer2: LogBuffer
@Mock private lateinit var table1: TableLogBuffer
@Mock private lateinit var table2: TableLogBuffer
private val dumpManager = DumpManager()
@Before
@@ -50,276 +49,144 @@ class DumpManagerTest : SysuiTestCase() {
}
@Test
fun testDumpTarget_dumpable() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
// WHEN a dumpable is dumped explicitly
val args = arrayOf<String>()
dumpManager.dumpTarget("dumpable2", pw, args, tailLength = 0)
// THEN only the requested one has their dump() method called
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2).dump(pw, args)
verify(dumpable3, never()).dump(any(), any())
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2, never()).dump(any(), anyInt())
}
@Test
fun testDumpTarget_buffer() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
// WHEN a buffer is dumped explicitly
val args = arrayOf<String>()
dumpManager.dumpTarget("buffer1", pw, args, tailLength = 14)
// THEN only the requested one has their dump() method called
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2, never()).dump(any(), any())
verify(dumpable3, never()).dump(any(), any())
verify(buffer1).dump(pw, tailLength = 14)
verify(buffer2, never()).dump(any(), anyInt())
}
@Test
fun testDumpableMatchingIsBasedOnEndOfTag() {
// GIVEN a dumpable registered to the manager
dumpManager.registerCriticalDumpable("com.android.foo.bar.dumpable1", dumpable1)
// WHEN that module is dumped
val args = arrayOf<String>()
dumpManager.dumpTarget("dumpable1", pw, arrayOf(), tailLength = 14)
// THEN its dump() method is called
verify(dumpable1).dump(pw, args)
}
@Test
fun testDumpTarget_selectsShortestNamedDumpable() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("first-dumpable", dumpable1)
dumpManager.registerCriticalDumpable("scnd-dumpable", dumpable2)
dumpManager.registerCriticalDumpable("third-dumpable", dumpable3)
// WHEN a dumpable is dumped by a suffix that matches multiple options
val args = arrayOf<String>()
dumpManager.dumpTarget("dumpable", pw, args, tailLength = 0)
// THEN the matching dumpable with the shorter name is dumped
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2).dump(pw, args)
verify(dumpable3, never()).dump(any(), any())
}
@Test
fun testDumpTarget_selectsShortestNamedBuffer() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerBuffer("first-buffer", buffer1)
dumpManager.registerBuffer("scnd-buffer", buffer2)
// WHEN a dumpable is dumped by a suffix that matches multiple options
val args = arrayOf<String>()
dumpManager.dumpTarget("buffer", pw, args, tailLength = 14)
// THEN the matching buffer with the shorter name is dumped
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2).dump(pw, tailLength = 14)
}
@Test
fun testDumpTarget_selectsShortestNamedMatch_dumpable() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("big-buffer1", buffer1)
dumpManager.registerBuffer("big-buffer2", buffer2)
// WHEN a dumpable is dumped by a suffix that matches multiple options
val args = arrayOf<String>()
dumpManager.dumpTarget("2", pw, args, tailLength = 14)
// THEN the matching dumpable with the shorter name is dumped
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2).dump(pw, args)
verify(dumpable3, never()).dump(any(), any())
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2, never()).dump(any(), anyInt())
}
@Test
fun testDumpTarget_selectsShortestNamedMatch_buffer() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
// WHEN a dumpable is dumped by a suffix that matches multiple options
val args = arrayOf<String>()
dumpManager.dumpTarget("2", pw, args, tailLength = 14)
// THEN the matching buffer with the shorter name is dumped
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2, never()).dump(any(), any())
verify(dumpable3, never()).dump(any(), any())
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2).dump(pw, tailLength = 14)
}
@Test
fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_dumpable() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("d1x", dumpable1)
dumpManager.registerCriticalDumpable("d2x", dumpable2)
dumpManager.registerCriticalDumpable("a3x", dumpable3)
dumpManager.registerBuffer("ab1x", buffer1)
dumpManager.registerBuffer("b2x", buffer2)
// WHEN a dumpable is dumped by a suffix that matches multiple options
val args = arrayOf<String>()
dumpManager.dumpTarget("x", pw, args, tailLength = 14)
// THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2, never()).dump(any(), any())
verify(dumpable3).dump(pw, args)
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2, never()).dump(any(), anyInt())
}
@Test
fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_buffer() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("d1x", dumpable1)
dumpManager.registerCriticalDumpable("d2x", dumpable2)
dumpManager.registerCriticalDumpable("az1x", dumpable3)
dumpManager.registerBuffer("b1x", buffer1)
dumpManager.registerBuffer("b2x", buffer2)
// WHEN a dumpable is dumped by a suffix that matches multiple options
val args = arrayOf<String>()
dumpManager.dumpTarget("x", pw, args, tailLength = 14)
// THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2, never()).dump(any(), any())
verify(dumpable3, never()).dump(any(), any())
verify(buffer1).dump(pw, tailLength = 14)
verify(buffer2, never()).dump(any(), anyInt())
}
@Test
fun testDumpDumpables() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerNormalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
// WHEN a dumpable dump is requested
val args = arrayOf<String>()
dumpManager.dumpDumpables(pw, args)
// THEN all dumpables are dumped (both critical and normal) (and no dumpables)
verify(dumpable1).dump(pw, args)
verify(dumpable2).dump(pw, args)
verify(dumpable3).dump(pw, args)
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2, never()).dump(any(), anyInt())
}
@Test
fun testDumpBuffers() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerNormalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
// WHEN a buffer dump is requested
dumpManager.dumpBuffers(pw, tailLength = 1)
// THEN all buffers are dumped (and no dumpables)
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2, never()).dump(any(), any())
verify(dumpable3, never()).dump(any(), any())
verify(buffer1).dump(pw, tailLength = 1)
verify(buffer2).dump(pw, tailLength = 1)
}
@Test
fun testCriticalDump() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerNormalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
// WHEN a critical dump is requested
val args = arrayOf<String>()
dumpManager.dumpCritical(pw, args)
// THEN only critical modules are dumped (and no buffers)
verify(dumpable1).dump(pw, args)
verify(dumpable2).dump(pw, args)
verify(dumpable3, never()).dump(any(), any())
verify(buffer1, never()).dump(any(), anyInt())
verify(buffer2, never()).dump(any(), anyInt())
}
@Test
fun testNormalDump() {
// GIVEN a variety of registered dumpables and buffers
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerNormalDumpable("dumpable3", dumpable3)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
// WHEN a normal dump is requested
val args = arrayOf<String>()
dumpManager.dumpNormal(pw, args, tailLength = 2)
// THEN the normal module and all buffers are dumped
verify(dumpable1, never()).dump(any(), any())
verify(dumpable2, never()).dump(any(), any())
verify(dumpable3).dump(pw, args)
verify(buffer1).dump(pw, tailLength = 2)
verify(buffer2).dump(pw, tailLength = 2)
}
@Test
fun testUnregister() {
// GIVEN a variety of registered dumpables and buffers
fun testRegisterUnregister_dumpables() {
// GIVEN a variety of registered dumpables
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerNormalDumpable("dumpable3", dumpable3)
// WHEN the collection is requested
var dumpables = dumpManager.getDumpables().map { it.dumpable }
// THEN it contains the registered entries
assertThat(dumpables).containsExactly(dumpable1, dumpable2, dumpable3)
// WHEN the dumpables are unregistered
dumpManager.unregisterDumpable("dumpable2")
dumpManager.unregisterDumpable("dumpable3")
// WHEN a dumpables dump is requested
val args = arrayOf<String>()
dumpManager.dumpDumpables(pw, args)
// WHEN the dumpable collection is requests
dumpables = dumpManager.getDumpables().map { it.dumpable }
// THEN the unregistered dumpables (both normal and critical) are not dumped
verify(dumpable1).dump(pw, args)
verify(dumpable2, never()).dump(any(), any())
verify(dumpable3, never()).dump(any(), any())
// THEN it contains only the currently-registered entry
assertThat(dumpables).containsExactly(dumpable1)
}
@Test
fun testRegister_buffers() {
// GIVEN a set of registered buffers
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
// WHEN the collection is requested
val dumpables = dumpManager.getLogBuffers().map { it.buffer }
// THEN it contains the registered entries
assertThat(dumpables).containsExactly(buffer1, buffer2)
}
@Test
fun testRegister_tableLogBuffers() {
// GIVEN a set of registered buffers
dumpManager.registerTableLogBuffer("table1", table1)
dumpManager.registerTableLogBuffer("table2", table2)
// WHEN the collection is requested
val tables = dumpManager.getTableLogBuffers().map { it.table }
// THEN it contains the registered entries
assertThat(tables).containsExactly(table1, table2)
}
@Test
fun registerDumpable_throwsWhenNameCannotBeAssigned() {
// GIVEN dumpable1 and buffer1 and table1 are registered
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerTableLogBuffer("table1", table1)
// THEN an exception is thrown when trying to re-register a new dumpable under the same key
assertThrows(IllegalArgumentException::class.java) {
dumpManager.registerCriticalDumpable("dumpable1", dumpable2)
}
assertThrows(IllegalArgumentException::class.java) {
dumpManager.registerBuffer("buffer1", buffer2)
}
assertThrows(IllegalArgumentException::class.java) {
dumpManager.registerTableLogBuffer("table1", table2)
}
}
@Test
fun registerDumpable_doesNotThrowWhenReRegistering() {
// GIVEN dumpable1 and buffer1 are registered
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerBuffer("buffer1", buffer1)
// THEN no exception is thrown when trying to re-register a new dumpable under the same key
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerBuffer("buffer1", buffer1)
// No exception thrown
}
@Test
fun getDumpables_returnsSafeCollection() {
// GIVEN a variety of registered dumpables
dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
dumpManager.registerNormalDumpable("dumpable3", dumpable3)
// WHEN the collection is retrieved
val dumpables = dumpManager.getDumpables()
// WHEN the collection changes from underneath
dumpManager.unregisterDumpable("dumpable1")
dumpManager.unregisterDumpable("dumpable2")
dumpManager.unregisterDumpable("dumpable3")
// THEN new collections are empty
assertThat(dumpManager.getDumpables()).isEmpty()
// AND the collection is still safe to use
assertThat(dumpables).hasSize(3)
}
@Test
fun getBuffers_returnsSafeCollection() {
// GIVEN a set of registered buffers
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
// WHEN the collection is requested
val buffers = dumpManager.getLogBuffers()
// WHEN the collection changes
dumpManager.registerBuffer("buffer3", buffer1)
// THEN the new entry is represented
assertThat(dumpManager.getLogBuffers()).hasSize(3)
// AND the previous collection is unchanged
assertThat(buffers).hasSize(2)
}
@Test
fun getTableBuffers_returnsSafeCollection() {
// GIVEN a set of registered buffers
dumpManager.registerTableLogBuffer("table1", table1)
dumpManager.registerTableLogBuffer("table2", table2)
// WHEN the collection is requested
val tables = dumpManager.getTableLogBuffers()
// WHEN the collection changes
dumpManager.registerTableLogBuffer("table3", table1)
// THEN the new entry is represented
assertThat(dumpManager.getTableLogBuffers()).hasSize(3)
// AND the previous collection is unchanged
assertThat(tables).hasSize(2)
}
}

View File

@@ -18,20 +18,14 @@ package com.android.systemui.dump
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpHandler.Companion.dump
import com.android.systemui.log.LogBuffer
import com.android.systemui.util.io.FakeBasicFileAttributes
import com.android.systemui.util.io.Files
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.io.BufferedWriter
import java.io.ByteArrayOutputStream
import java.io.IOException
@@ -42,17 +36,29 @@ import java.nio.file.OpenOption
import java.nio.file.Paths
import java.nio.file.attribute.BasicFileAttributes
import java.util.Arrays
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
class LogEulogizerTest : SysuiTestCase() {
lateinit var eulogizer: LogBufferEulogizer
@Mock
lateinit var dumpManager: DumpManager
@Mock lateinit var dumpManager: DumpManager
@Mock lateinit var logBuffer1: LogBuffer
lateinit var logBufferEntry1: DumpsysEntry.LogBufferEntry
@Mock lateinit var logBuffer2: LogBuffer
lateinit var logBufferEntry2: DumpsysEntry.LogBufferEntry
@Mock
lateinit var files: Files
@Mock lateinit var files: Files
private val clock = FakeSystemClock()
@@ -67,37 +73,47 @@ class LogEulogizerTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
logBufferEntry1 = DumpsysEntry.LogBufferEntry(logBuffer1, "logbuffer1")
logBufferEntry2 = DumpsysEntry.LogBufferEntry(logBuffer2, "logbuffer2")
eulogizer =
LogBufferEulogizer(dumpManager, clock, files, path, MIN_WRITE_GAP, MAX_READ_AGE)
eulogizer = LogBufferEulogizer(dumpManager, clock, files, path, MIN_WRITE_GAP, MAX_READ_AGE)
Mockito.`when`(files.newBufferedWriter(eq(path), any(OpenOption::class.java)))
.thenReturn(fileWriter)
.thenReturn(fileWriter)
Mockito.`when`(
files.readAttributes(eq(path),
eq(BasicFileAttributes::class.java),
any(LinkOption::class.java))
).thenReturn(fileAttrs)
files.readAttributes(
eq(path),
eq(BasicFileAttributes::class.java),
any(LinkOption::class.java)
)
)
.thenReturn(fileAttrs)
Mockito.`when`(files.lines(eq(path))).thenReturn(Arrays.stream(FAKE_LINES))
whenever(dumpManager.getLogBuffers()).thenReturn(listOf(logBufferEntry1, logBufferEntry2))
}
@Test
fun testFileIsCreated() {
// GIVEN that the log file doesn't already exist
Mockito.`when`(
files.readAttributes(eq(path),
eq(BasicFileAttributes::class.java),
any(LinkOption::class.java))
).thenThrow(IOException("File not found"))
files.readAttributes(
eq(path),
eq(BasicFileAttributes::class.java),
any(LinkOption::class.java)
)
)
.thenThrow(IOException("File not found"))
// WHEN .record() is called
val exception = RuntimeException("Something bad happened")
assertEquals(exception, eulogizer.record(exception))
// THEN the buffers are dumped to the file
verify(dumpManager).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt())
verify(logBuffer1).dump(any(PrintWriter::class.java), anyInt())
verify(logBuffer2).dump(any(PrintWriter::class.java), anyInt())
assertTrue(fileStream.toString().isNotEmpty())
}
@@ -111,7 +127,8 @@ class LogEulogizerTest : SysuiTestCase() {
assertEquals(exception, eulogizer.record(exception))
// THEN the buffers are dumped to the file
verify(dumpManager).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt())
verify(logBuffer1).dump(any(PrintWriter::class.java), anyInt())
verify(logBuffer2).dump(any(PrintWriter::class.java), anyInt())
assertTrue(fileStream.toString().isNotEmpty())
}
@@ -125,7 +142,8 @@ class LogEulogizerTest : SysuiTestCase() {
assertEquals(exception, eulogizer.record(exception))
// THEN the file isn't written to
verify(dumpManager, never()).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt())
verify(logBuffer1, never()).dump(any(PrintWriter::class.java), anyInt())
verify(logBuffer2, never()).dump(any(PrintWriter::class.java), anyInt())
assertTrue(fileStream.toString().isEmpty())
}
@@ -161,9 +179,4 @@ class LogEulogizerTest : SysuiTestCase() {
private const val MIN_WRITE_GAP = 10L
private const val MAX_READ_AGE = 100L
private val FAKE_LINES =
arrayOf(
"First line",
"Second line",
"Third line"
)
private val FAKE_LINES = arrayOf("First line", "Second line", "Third line")