PerfettoTraceProcessor.kt
/*
* Copyright 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 androidx.benchmark.perfetto
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
import androidx.benchmark.macro.perfetto.server.PerfettoHttpServer
import androidx.benchmark.userspaceTrace
import java.io.File
import org.intellij.lang.annotations.Language
import perfetto.protos.QueryResult
import perfetto.protos.TraceMetrics
/**
* Kotlin API for [Perfetto Trace Processor](https://perfetto.dev/docs/analysis/trace-processor),
* which enables SQL querying against the data stored in a Perfetto trace.
*
* This includes synchronous and async trace sections, kernel-level scheduling timing,
* binder events... If it's displayed in Android Studio system trace or
* [ui.perfetto.dev](https://ui.perfetto.dev), it can be queried from this API.
*
* ```
* // Collect the duration of all slices named "activityStart" in the trace
* val activityStartDurationNs = PerfettoTraceProcessor.runServer {
* loadTrace(trace) {
* query("SELECT dur FROM slice WHERE name LIKE \"activityStart\"").toList {
* it.long("dur")
* }
* }
* }
* ```
*
* Note that traces generally hold events from multiple apps, services and processes, so it's
* recommended to filter potentially common trace events to the process you're interested in. See
* the following example which queries `Choreographer#doFrame` slices (labelled spans of time) only
* for a given package name:
*
* ```
* query("""
* |SELECT
* | slice.name,slice.ts,slice.dur
* |FROM slice
* | INNER JOIN thread_track on slice.track_id = thread_track.id
* | INNER JOIN thread USING(utid)
* | INNER JOIN process USING(upid)
* |WHERE
* | slice.name LIKE "Choreographer#doFrame%" AND
* | process.name LIKE "$packageName"
* """.trimMargin()
* )
* ```
* See also Perfetto project documentation:
* * [Trace Processor overview](https://perfetto.dev/docs/analysis/trace-processor)
* * [Common queries](https://perfetto.dev/docs/analysis/common-queries)
*
* @see PerfettoTrace
*/
@ExperimentalPerfettoTraceProcessorApi
class PerfettoTraceProcessor {
companion object {
internal const val PORT = 9001
/**
* The actual [File] path to the `trace_processor_shell`.
*
* Lazily copies the `trace_processor_shell` and enables parsing of the Perfetto trace files.
*/
internal val shellPath: String by lazy {
// Checks for ABI support
PerfettoHelper.createExecutable("trace_processor_shell")
}
/**
* Starts a Perfetto trace processor shell server in http mode, loads a trace and executes
* the given block. It stops the server after the block is complete
*/
@JvmStatic
fun <T> runServer(
block: PerfettoTraceProcessor.() -> T
): T = userspaceTrace("PerfettoTraceProcessor#runServer") {
var perfettoTraceProcessor: PerfettoTraceProcessor? = null
try {
// Initializes the server process
perfettoTraceProcessor = PerfettoTraceProcessor().startServer()
// Executes the query block
return@userspaceTrace userspaceTrace("PerfettoTraceProcessor#runServer#block") {
block(perfettoTraceProcessor)
}
} finally {
perfettoTraceProcessor?.stopServer()
}
}
@RestrictTo(LIBRARY_GROUP)
fun <T> runSingleSessionServer(
absoluteTracePath: String,
block: Session.() -> T
) = runServer {
loadTrace(PerfettoTrace(absoluteTracePath)) {
block(this)
}
}
}
/**
* Loads a PerfettoTrace into the trace processor server to query data out of the trace.
*/
fun <T> loadTrace(
trace: PerfettoTrace,
block: Session.() -> T
): T {
loadTraceImpl(trace.path)
// TODO: unload trace after block
return block.invoke(Session(this))
}
class Session internal constructor(
private val traceProcessor: PerfettoTraceProcessor
) {
/**
* Computes the given metric on the previously loaded trace.
*/
@RestrictTo(LIBRARY_GROUP) // avoids exposing Proto API
fun getTraceMetrics(metric: String): TraceMetrics {
userspaceTrace("PerfettoTraceProcessor#getTraceMetrics $metric") {
require(!metric.contains(" ")) {
"Metric must not contain spaces: $metric"
}
require(traceProcessor.perfettoHttpServer.isRunning()) {
"Perfetto trace_shell_process is not running."
}
// Compute metrics
val computeResult = traceProcessor.perfettoHttpServer.computeMetric(listOf(metric))
if (computeResult.error != null) {
throw IllegalStateException(computeResult.error)
}
// Decode and return trace metrics
return TraceMetrics.ADAPTER.decode(computeResult.metrics!!)
}
}
/**
* Computes the given query on the currently loaded trace.
*
* Each row returned by a query is returned by the `Sequence` as a [Row]. To extract data
* from a `Row`, query by column name. The following example does this for name, timestamp,
* and duration of slices:
* ```
* // Runs the provided callback on each activityStart instance in the trace,
* // providing name, start timestamp (in ns) and duration (in ns)
* fun PerfettoTraceProcessor.Session.forEachActivityStart(callback: (String, Long, Long) -> Unit) {
* query("SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\"").forEach {
* callback(it.string("name"), it.long("ts"), it.long("dur")
* // or, used as a map:
* //callback(it["name"] as String, it["ts"] as Long, it["dur"] as Long)
* }
* }
* ```
*/
fun query(@Language("sql") query: String): Sequence<Row> {
userspaceTrace("PerfettoTraceProcessor#query $query".take(127)) {
require(traceProcessor.perfettoHttpServer.isRunning()) {
"Perfetto trace_shell_process is not running."
}
val queryResult = traceProcessor.perfettoHttpServer.query(query) {
QueryResult.ADAPTER.decode(it)
}
return Sequence { QueryResultIterator(queryResult) }
}
}
/**
* Computes the given query on the currently loaded trace, returning the resulting protobuf
* bytes as a [ByteArray].
*
* Use [Session.query] if you do not wish to parse the Proto result yourself.
*
* The `QueryResult` protobuf definition can be found
* [in the Perfetto project](https://github.com/google/perfetto/blob/master/protos/perfetto/trace_processor/trace_processor.proto),
* which can be used to decode the result returned here with a protobuf parsing library.
*
* @see Session.query
*/
fun queryBytes(@Language("sql") query: String): ByteArray {
userspaceTrace("PerfettoTraceProcessor#query $query".take(127)) {
require(traceProcessor.perfettoHttpServer.isRunning()) {
"Perfetto trace_shell_process is not running."
}
return traceProcessor.perfettoHttpServer.query(query) { it.readBytes() }
}
}
/**
* Query a trace for a list of slices - name, timestamp, and duration.
*
* Note that sliceNames may include wildcard matches, such as `foo%`
*/
@RestrictTo(LIBRARY_GROUP) // Slice API not currently exposed, since it doesn't track table
fun querySlices(vararg sliceNames: String): List<Slice> {
require(traceProcessor.perfettoHttpServer.isRunning()) {
"Perfetto trace_shell_process is not running."
}
val whereClause = sliceNames
.joinToString(separator = " OR ") {
"slice.name LIKE \"$it\""
}
val queryResultIterator = query(
query = """
SELECT slice.name,ts,dur
FROM slice
WHERE $whereClause
""".trimMargin()
)
return queryResultIterator.toSlices()
}
}
private val perfettoHttpServer: PerfettoHttpServer = PerfettoHttpServer()
private var traceLoaded = false
private fun startServer(): PerfettoTraceProcessor =
userspaceTrace("PerfettoTraceProcessor#startServer") {
println("startserver")
perfettoHttpServer.startServer()
return@userspaceTrace this
}
private fun stopServer() = userspaceTrace("PerfettoTraceProcessor#stopServer") {
println("stopserver")
perfettoHttpServer.stopServer()
}
/**
* Loads a trace in the current instance of the trace processor, clearing any previous loaded
* trace if existing.
*/
private fun loadTraceImpl(absoluteTracePath: String) {
userspaceTrace("PerfettoTraceProcessor#loadTraceImpl") {
require(!absoluteTracePath.contains(" ")) {
"Trace path must not contain spaces: $absoluteTracePath"
}
val traceFile = File(absoluteTracePath)
require(traceFile.exists() && traceFile.isFile) {
"Trace path must exist and not be a directory: $absoluteTracePath"
}
// In case a previous trace was loaded, ensures to clear
if (traceLoaded) {
clearTrace()
}
val parseResult = perfettoHttpServer.parse(traceFile.readBytes())
if (parseResult.error != null) {
throw IllegalStateException(parseResult.error)
}
// Notifies the server that it won't receive any more trace parts
perfettoHttpServer.notifyEof()
traceLoaded = true
}
}
/**
* Clears the current loaded trace.
*/
private fun clearTrace() = userspaceTrace("PerfettoTraceProcessor#clearTrace") {
perfettoHttpServer.restoreInitialTables()
traceLoaded = false
}
}
/**
* Helper for fuzzy matching process name to package
*/
internal fun processNameLikePkg(pkg: String): String {
return """(process.name LIKE "$pkg" OR process.name LIKE "$pkg:%")"""
}