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

Commit f405ea09 authored by Peter Kalauskas's avatar Peter Kalauskas
Browse files

Flow extensions for coroutine tracing

Flag: com.android.systemui.coroutine_tracing
Test: Capture perfetto trace during screen lock/unlock
Bug: 350931144
Bug: 358541864
Change-Id: I639a658743c4ec7b8e24494fc614331f00e759b8
parent 87b99b86
Loading
Loading
Loading
Loading

tracinglib/Android.bp

0 → 100644
+19 −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 {
    default_team: "trendy_team_performance",
}
+16 −14
Original line number Diff line number Diff line
@@ -27,20 +27,22 @@ java_library {
    srcs: ["android/src-platform-api/**/*.kt"],
}

java_library {
    name: "tracinglib-androidx",
    defaults: ["tracinglib-defaults"],
    static_libs: [
        "kotlinx_coroutines_android",
        "com_android_systemui_flags_lib",
        "//frameworks/libs/systemui:compilelib",
        "androidx.tracing_tracing",
    ],
    srcs: ["android/src-public-api/**/*.kt"],
    sdk_version: "31",
    min_sdk_version: "19",
    java_version: "17",
}
// TODO(b/358541864): Remove source files of "tracinglib-androidx" and delete this target
// Disable due to compilation error using java.lang.StackWalker:
// java_library {
//     name: "tracinglib-androidx",
//     defaults: ["tracinglib-defaults"],
//     static_libs: [
//         "kotlinx_coroutines_android",
//         "com_android_systemui_flags_lib",
//         "//frameworks/libs/systemui:compilelib",
//         "androidx.tracing_tracing",
//     ],
//     srcs: ["android/src-public-api/**/*.kt"],
//     sdk_version: "31",
//     min_sdk_version: "19",
//     java_version: "17",
// }

java_test_host {
    name: "tracinglib-host-test",
+134 −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.app.tracing.coroutines.flow

import com.android.app.tracing.coroutines.traceCoroutine
import com.android.app.tracing.traceBegin
import com.android.app.tracing.traceEnd
import kotlin.experimental.ExperimentalTypeInference
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.collectLatest as kx_collectLatest
import kotlinx.coroutines.flow.filter as kx_filter
import kotlinx.coroutines.flow.filterIsInstance as kx_filterIsInstance
import kotlinx.coroutines.flow.flowOn as kx_flowOn
import kotlinx.coroutines.flow.map as kx_map

fun <T> Flow<T>.withTraceName(name: String?): Flow<T> {
    return object : Flow<T> {
        override suspend fun collect(collector: FlowCollector<T>) {
            this@withTraceName.collect("${name ?: walkStackForClassName()}", collector)
        }
    }
}

/**
 * NOTE: We cannot use a default value for the String name because [Flow.collect] is a member
 * function. When an extension function has the same receiver type, name, and applicable arguments
 * as a class member function, the member takes precedence.
 */
@OptIn(ExperimentalTypeInference::class)
suspend inline fun <T> Flow<T>.collect(
    name: String, /* cannot have a default parameter or else Flow#collect() override this call */
    @BuilderInference block: FlowCollector<T>,
) {
    val (collectSlice, emitSlice) = getFlowSliceNames(name)
    traceCoroutine(collectSlice) {
        collect { value -> traceCoroutine(emitSlice) { block.emit(value) } }
    }
}

@OptIn(ExperimentalTypeInference::class)
suspend inline fun <T> Flow<T>.collectTraced(
    @BuilderInference block: FlowCollector<T>,
) {
    collect(walkStackForClassName(), block)
}

suspend fun <T> Flow<T>.collectLatest(name: String? = null, action: suspend (T) -> Unit) {
    val (collectSlice, emitSlice) = getFlowSliceNames(name)
    traceCoroutine(collectSlice) {
        kx_collectLatest { value -> traceCoroutine(emitSlice) { action(value) } }
    }
}

fun <T> Flow<T>.flowOn(context: kotlin.coroutines.CoroutineContext): Flow<T> {
    val contextName =
        context[CoroutineName]?.name
            ?: context[CoroutineDispatcher]?.javaClass?.simpleName
            ?: context.javaClass.simpleName
    return kx_flowOn(context).withTraceName("flowOn($contextName)")
}

inline fun <T> Flow<T>.filter(
    name: String? = null,
    crossinline predicate: suspend (T) -> Boolean,
): Flow<T> {
    val flowName = "${name ?: walkStackForClassName()}"
    return withTraceName(flowName).kx_filter {
        return@kx_filter traceCoroutine("$flowName:predicate") { predicate(it) }
    }
}

inline fun <reified R> Flow<*>.filterIsInstance(): Flow<R> {
    return kx_filterIsInstance<R>().withTraceName("${walkStackForClassName()}#filterIsInstance")
}

inline fun <T, R> Flow<T>.map(
    name: String? = null,
    crossinline transform: suspend (T) -> R,
): Flow<R> {
    val flowName = "${name ?: walkStackForClassName()}"
    return withTraceName(flowName).kx_map {
        return@kx_map traceCoroutine("$flowName:transform") { transform(it) }
    }
}

fun getFlowSliceNames(name: String?): Pair<String, String> {
    val flowName = "${name ?: walkStackForClassName()}"
    return Pair("$flowName:collect", "$flowName:emit")
}

object FlowExt {
    val currentFileName: String =
        StackWalker.getInstance().walk { stream -> stream.limit(1).findFirst() }.get().fileName
}

private fun isFrameInteresting(frame: StackWalker.StackFrame): Boolean {
    return frame.fileName != FlowExt.currentFileName
}

/** Get a name for the trace section include the name of the call site. */
fun walkStackForClassName(): String {
    traceBegin("FlowExt#walkStackForClassName")
    try {
        val interestingFrame =
            StackWalker.getInstance().walk { stream ->
                stream.filter(::isFrameInteresting).limit(5).findFirst()
            }
        return if (interestingFrame.isPresent) {
            val frame = interestingFrame.get()
            return frame.className
        } else {
            "<unknown>"
        }
    } finally {
        traceEnd()
    }
}
+31 −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.app.tracing.coroutines

class ExampleClass(
    private val testBase: TestBase,
    private val incrementCounter: suspend () -> Unit,
) {
    suspend fun classMethod(value: Int) {
        testBase.expect(
            "launch-for-collect",
            "com.android.app.tracing.coroutines.FlowTracingTest\$stateFlowCollection$1\$collectJob$1$3:collect",
            "com.android.app.tracing.coroutines.FlowTracingTest\$stateFlowCollection$1\$collectJob$1$3:emit"
        )
        incrementCounter()
    }
}
+144 −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.app.tracing.coroutines

import com.android.app.tracing.coroutines.flow.collect
import com.android.app.tracing.coroutines.flow.collectTraced
import com.android.app.tracing.coroutines.flow.filter
import com.android.app.tracing.coroutines.flow.flowOn
import com.android.app.tracing.coroutines.flow.map
import com.android.app.tracing.coroutines.flow.withTraceName
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.newFixedThreadPoolContext
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.withContext
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.BlockJUnit4ClassRunner

@RunWith(BlockJUnit4ClassRunner::class)
class FlowTracingTest : TestBase() {
    @Test
    fun stateFlowCollection() = runTestWithTraceContext {
        val state = MutableStateFlow(1)
        val bgThreadPool = newFixedThreadPoolContext(2, "bg-pool")

        // Inefficient fine-grained thread confinement
        val counterThread = newSingleThreadContext("counter-thread")
        var counter = 0
        val incrementCounter: suspend () -> Unit = {
            withContext("increment", counterThread) {
                expectEndsWith("increment")
                counter++
            }
        }

        val helper = ExampleClass(this@FlowTracingTest, incrementCounter)
        val collectJob =
            launch("launch-for-collect", bgThreadPool) {
                expect("launch-for-collect")
                launch {
                    state.collect("state-flow") {
                        expect("launch-for-collect", "state-flow:collect", "state-flow:emit")
                        incrementCounter()
                    }
                }
                launch {
                    state.collectTraced {
                        expect(
                            "launch-for-collect",
                            "com.android.app.tracing.coroutines.FlowTracingTest\$stateFlowCollection$1\$collectJob$1$2:collect",
                            "com.android.app.tracing.coroutines.FlowTracingTest\$stateFlowCollection$1\$collectJob$1$2:emit"
                        )
                        incrementCounter()
                    }
                }
                launch { state.collectTraced(helper::classMethod) }
            }
        val emitJob =
            launch(newSingleThreadContext("emitter thread")) {
                for (n in 2..5) {
                    delay(100)
                    state.value = n
                }
            }
        emitJob.join()
        delay(10)
        collectJob.cancel()
        withContext(counterThread) { assertEquals(15, counter) }
    }

    @Test
    fun flowOnWithTraceName() = runTestWithTraceContext {
        val state =
            flowOf(1, 2, 3, 4)
                .withTraceName("my-flow")
                .flowOn(
                    newSingleThreadContext("flow-thread") +
                        EmptyCoroutineContext +
                        CoroutineName("the-name")
                )
        val bgThreadPool = newFixedThreadPoolContext(2, "bg-pool")
        val collectJob =
            launch("launch-for-collect", bgThreadPool) {
                expect("launch-for-collect")
                launch {
                    state.collect("state-flow") {
                        expect(
                            "launch-for-collect",
                            "state-flow:collect",
                            "flowOn(the-name):collect",
                            "flowOn(the-name):emit",
                            "state-flow:emit"
                        )
                    }
                }
            }
        collectJob.join()
    }

    @Test
    fun mapAndFilter() = runTestWithTraceContext {
        val state =
            flowOf(1, 2, 3, 4)
                .withTraceName("my-flow")
                .map("multiply-by-3") { it * 2 }
                .filter("mod-2") { it % 2 == 0 }
        launch("launch-for-collect") {
                state.collect("my-collect-call") {
                    expect(
                        "launch-for-collect",
                        "my-collect-call:collect",
                        "mod-2:collect",
                        "multiply-by-3:collect",
                        "my-flow:collect",
                        "my-flow:emit",
                        "multiply-by-3:emit",
                        "mod-2:emit",
                        "my-collect-call:emit"
                    )
                }
            }
            .join()
    }
}
Loading