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

Commit b40eb0e0 authored by Jeff DeCew's avatar Jeff DeCew Committed by Android (Google) Code Review
Browse files

Merge changes I51b64002,I4ea82f1b into main

* changes:
  Add a FlowDumper utility that makes it easy to mark a flow such that it will be dumped.
  Allow dumping all dumpables that match a given suffix.
parents cb8f552c 7b35ec70
Loading
Loading
Loading
Loading
+71 −31
Original line number Diff line number Diff line
@@ -222,13 +222,18 @@ constructor(
            val buffers = dumpManager.getLogBuffers()
            val tableBuffers = dumpManager.getTableLogBuffers()

            targets.forEach { target ->
                findTargetInCollection(target, dumpables, buffers, tableBuffers)?.dump(pw, args)
            val matches =
                if (args.matchAll) {
                    findAllMatchesInCollection(targets, dumpables, buffers, tableBuffers)
                } else {
                    findBestMatchesInCollection(targets, dumpables, buffers, tableBuffers)
                }
            matches.forEach { it.dump(pw, args) }
        } else {
            if (args.listOnly) {
                val dumpables = dumpManager.getDumpables()
                val buffers = dumpManager.getLogBuffers()
                val tableBuffers = dumpManager.getTableLogBuffers()

                pw.println("Dumpables:")
                listTargetNames(dumpables, pw)
@@ -236,18 +241,23 @@ constructor(

                pw.println("Buffers:")
                listTargetNames(buffers, pw)
                pw.println()

                pw.println("TableBuffers:")
                listTargetNames(tableBuffers, pw)
            } else {
                pw.println("Nothing to dump :(")
            }
        }
    }

    /** Finds the best match for a particular target */
    private fun findTargetInCollection(
        target: String,
        dumpables: Collection<DumpableEntry>,
        logBuffers: Collection<LogBufferEntry>,
        tableBuffers: Collection<TableLogBufferEntry>,
    ) =
    ): DumpsysEntry? =
        sequence {
                findBestTargetMatch(dumpables, target)?.let { yield(it) }
                findBestTargetMatch(logBuffers, target)?.let { yield(it) }
@@ -256,6 +266,31 @@ constructor(
            .sortedBy { it.name }
            .minByOrNull { it.name.length }

    /** Finds the best match for each target, if any, in the order of the targets */
    private fun findBestMatchesInCollection(
        targets: List<String>,
        dumpables: Collection<DumpableEntry>,
        logBuffers: Collection<LogBufferEntry>,
        tableBuffers: Collection<TableLogBufferEntry>,
    ): List<DumpsysEntry> =
        targets.mapNotNull { target ->
            findTargetInCollection(target, dumpables, logBuffers, tableBuffers)
        }

    /** Finds all matches for any target, returning in the --list order. */
    private fun findAllMatchesInCollection(
        targets: List<String>,
        dumpables: Collection<DumpableEntry>,
        logBuffers: Collection<LogBufferEntry>,
        tableBuffers: Collection<TableLogBufferEntry>,
    ): List<DumpsysEntry> =
        sequence {
                yieldAll(dumpables.filter { it.matchesAny(targets) })
                yieldAll(logBuffers.filter { it.matchesAny(targets) })
                yieldAll(tableBuffers.filter { it.matchesAny(targets) })
            }
            .sortedBy { it.name }.toList()

    private fun dumpConfig(pw: PrintWriter) {
        config.dump(pw, arrayOf())
    }
@@ -272,6 +307,11 @@ constructor(
        pw.println("etc.")
        pw.println()

        pw.println("Print all matches, instead of the best match:")
        pw.println("$ <invocation> --all <targets>")
        pw.println("$ <invocation> --all Log")
        pw.println()

        pw.println("Special commands:")
        pw.println("$ <invocation> dumpables")
        pw.println("$ <invocation> buffers")
@@ -325,9 +365,10 @@ constructor(
                    "--help" -> {
                        pArgs.command = "help"
                    }
                    // This flag is passed as part of the proto dump in Bug reports, we can ignore
                    // it because this is our default behavior.
                    "-a" -> {}
                    "-a",
                    "--all" -> {
                        pArgs.matchAll = true
                    }
                    else -> {
                        throw ArgParseException("Unknown flag: $arg")
                    }
@@ -386,15 +427,19 @@ constructor(
        const val DUMPSYS_DUMPABLE_DIVIDER =
            "----------------------------------------------------------------------------"

        private fun DumpsysEntry.matches(target: String) = name.endsWith(target)
        private fun DumpsysEntry.matchesAny(targets: Collection<String>) =
            targets.any { matches(it) }

        private fun findBestTargetMatch(c: Collection<DumpsysEntry>, target: String) =
            c.asSequence().filter { it.name.endsWith(target) }.minByOrNull { it.name.length }
            c.asSequence().filter { it.matches(target) }.minByOrNull { it.name.length }

        private fun findBestProtoTargetMatch(
            c: Collection<DumpableEntry>,
            target: String
        ): ProtoDumpable? =
            c.asSequence()
                .filter { it.name.endsWith(target) }
                .filter { it.matches(target) }
                .filter { it.dumpable is ProtoDumpable }
                .minByOrNull { it.name.length }
                ?.dumpable as? ProtoDumpable
@@ -440,28 +485,24 @@ constructor(
        }

        /**
         * Utility to write a [DumpableEntry] to the given [PrintWriter] in a
         * dumpsys-appropriate format.
         * Utility to write a [DumpableEntry] to the given [PrintWriter] in a dumpsys-appropriate
         * format.
         */
        private fun dumpDumpable(
            entry: DumpableEntry,
            pw: PrintWriter,
            args: Array<String> = arrayOf(),
        ) = pw.wrapSection(entry) {
            entry.dumpable.dump(pw, args)
        }
        ) = pw.wrapSection(entry) { entry.dumpable.dump(pw, args) }

        /**
         * Utility to write a [LogBufferEntry] to the given [PrintWriter] in a
         * dumpsys-appropriate format.
         * Utility to write a [LogBufferEntry] to the given [PrintWriter] in a dumpsys-appropriate
         * format.
         */
        private fun dumpBuffer(
            entry: LogBufferEntry,
            pw: PrintWriter,
            tailLength: Int = 0,
        ) = pw.wrapSection(entry) {
            entry.buffer.dump(pw, tailLength)
        }
        ) = pw.wrapSection(entry) { entry.buffer.dump(pw, tailLength) }

        /**
         * Utility to write a [TableLogBufferEntry] to the given [PrintWriter] in a
@@ -471,9 +512,7 @@ constructor(
            entry: TableLogBufferEntry,
            pw: PrintWriter,
            args: Array<String> = arrayOf(),
        ) = pw.wrapSection(entry) {
            entry.table.dump(pw, args)
        }
        ) = pw.wrapSection(entry) { entry.table.dump(pw, args) }

        /**
         * Zero-arg utility to write a [DumpsysEntry] to the given [PrintWriter] in a
@@ -513,6 +552,7 @@ private class ParsedArgs(val rawArgs: Array<String>, val nonFlagArgs: List<Strin
    var tailLength: Int = 0
    var command: String? = null
    var listOnly = false
    var matchAll = false
    var proto = false
}

+142 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.util.kotlin

import android.util.IndentingPrintWriter
import com.android.systemui.Dumpable
import com.android.systemui.dump.DumpManager
import com.android.systemui.util.asIndenting
import com.android.systemui.util.printCollection
import java.io.PrintWriter
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow

/**
 * An interface which gives the implementing type flow extension functions which will register a
 * given flow as a field in the Dumpable.
 */
interface FlowDumper : Dumpable {
    /**
     * Include the last emitted value of this Flow whenever it is being collected. Remove its value
     * when collection ends.
     *
     * @param dumpName the name to use for this field in the dump output
     */
    fun <T> Flow<T>.dumpWhileCollecting(dumpName: String): Flow<T>

    /**
     * Include the [SharedFlow.replayCache] for this Flow in the dump.
     *
     * @param dumpName the name to use for this field in the dump output
     */
    fun <T, F : SharedFlow<T>> F.dumpReplayCache(dumpName: String): F

    /**
     * Include the [StateFlow.value] for this Flow in the dump.
     *
     * @param dumpName the name to use for this field in the dump output
     */
    fun <T, F : StateFlow<T>> F.dumpValue(dumpName: String): F

    /** The default [Dumpable.dump] implementation which just calls [dumpFlows] */
    override fun dump(pw: PrintWriter, args: Array<out String>) = dumpFlows(pw.asIndenting())

    /** Dump all the values from any registered / active Flows. */
    fun dumpFlows(pw: IndentingPrintWriter)
}

/**
 * An implementation of [FlowDumper]. This be extended directly, or can be used to implement
 * [FlowDumper] by delegation.
 *
 * @param dumpManager if provided, this will be used by the [FlowDumperImpl] to register and
 *   unregister itself when there is something to dump.
 * @param tag a static name by which this [FlowDumperImpl] is registered. If not provided, this
 *   class's name will be used. If you're implementing by delegation, you probably want to provide
 *   this tag to get a meaningful dumpable name.
 */
open class FlowDumperImpl(private val dumpManager: DumpManager?, tag: String? = null) : FlowDumper {
    private val stateFlowMap = ConcurrentHashMap<String, StateFlow<*>>()
    private val sharedFlowMap = ConcurrentHashMap<String, SharedFlow<*>>()
    private val flowCollectionMap = ConcurrentHashMap<Pair<String, String>, Any>()
    override fun dumpFlows(pw: IndentingPrintWriter) {
        pw.printCollection("StateFlow (value)", stateFlowMap.toSortedMap().entries) { (key, flow) ->
            append(key).append('=').println(flow.value)
        }
        pw.printCollection("SharedFlow (replayCache)", sharedFlowMap.toSortedMap().entries) {
            (key, flow) ->
            append(key).append('=').println(flow.replayCache)
        }
        val comparator = compareBy<Pair<String, String>> { it.first }.thenBy { it.second }
        pw.printCollection("Flow (latest)", flowCollectionMap.toSortedMap(comparator).entries) {
            (pair, value) ->
            append(pair.first).append('=').println(value)
        }
    }

    private val Any.idString: String
        get() = Integer.toHexString(System.identityHashCode(this))

    override fun <T> Flow<T>.dumpWhileCollecting(dumpName: String): Flow<T> = flow {
        val mapKey = dumpName to idString
        try {
            collect {
                flowCollectionMap[mapKey] = it ?: "null"
                updateRegistration(required = true)
                emit(it)
            }
        } finally {
            flowCollectionMap.remove(mapKey)
            updateRegistration(required = false)
        }
    }

    override fun <T, F : StateFlow<T>> F.dumpValue(dumpName: String): F {
        stateFlowMap[dumpName] = this
        return this
    }

    override fun <T, F : SharedFlow<T>> F.dumpReplayCache(dumpName: String): F {
        sharedFlowMap[dumpName] = this
        return this
    }

    private val dumpManagerName = tag ?: "[$idString] ${javaClass.simpleName}"
    private var registered = AtomicBoolean(false)
    private fun updateRegistration(required: Boolean) {
        if (dumpManager == null) return
        if (required && registered.get()) return
        synchronized(registered) {
            val shouldRegister =
                stateFlowMap.isNotEmpty() ||
                    sharedFlowMap.isNotEmpty() ||
                    flowCollectionMap.isNotEmpty()
            val wasRegistered = registered.getAndSet(shouldRegister)
            if (wasRegistered != shouldRegister) {
                if (shouldRegister) {
                    dumpManager.registerCriticalDumpable(dumpManagerName, this)
                } else {
                    dumpManager.unregisterDumpable(dumpManagerName)
                }
            }
        }
    }
}