CpuEventCounter.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

import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import java.io.Closeable

/**
 * Exposes CPU counters from perf_event_open based on libs/utils/src/Profiler.cpp from
 * Google/Filament.
 *
 * This layer is extremely simple to reduce overhead, though it does not yet use
 * fast/critical JNI.
 *
 * This counter must be closed to avoid leaking the associated native allocation.
 *
 * This class does not yet help callers with prerequisites to getting counter values on API 30+:
 *  - setenforce 0 (requires root)
 *  - security.perf_harden 0
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class CpuEventCounter : Closeable {
    private var profilerPtr = CpuCounterJni.newProfiler()
    private var hasReset = false

    fun resetEvents(events: List<Event>) {
        resetEvents(events.getFlags())
    }

    fun resetEvents(eventFlags: Int) {
        hasReset = true
        CpuCounterJni.resetEvents(profilerPtr, eventFlags)
    }

    override fun close() {
        CpuCounterJni.freeProfiler(profilerPtr)
        profilerPtr = 0
    }

    fun reset() {
        CpuCounterJni.reset(profilerPtr)
    }
    fun start() = CpuCounterJni.start(profilerPtr)
    fun stop() = CpuCounterJni.stop(profilerPtr)

    fun read(outValues: Values) {
        check(profilerPtr != 0L) { "Error: attempted to read counters after close" }
        check(hasReset) { "Error: attempted to read counters without reset" }
        CpuCounterJni.read(profilerPtr, outValues.longArray)
    }

    enum class Event(
        val id: Int
    ) {
        Instructions(0),
        CpuCycles(1),
        L1DReferences(2),
        L1DMisses(3),
        BranchInstructions(4),
        BranchMisses(5),
        L1IReferences(6),
        L1IMisses(7);

        val flag: Int
            inline get() = 1 shl id
    }

    /**
     * Holder class for querying all counter values at once out of native, to avoid multiple JNI
     * transitions.
     */
    @JvmInline
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    value class Values(val longArray: LongArray = LongArray(19)) {
        init {
            // See CountersLongCount static_assert in native
            require(longArray.size == 19)
        }

        inline val numberOfCounters: Long
            get() = longArray[0]
        inline val timeEnabled: Long
            get() = longArray[1]
        inline val timeRunning: Long
            get() = longArray[2]

        @Suppress("NOTHING_TO_INLINE")
        inline fun getValue(spec: Event): Long = longArray[3 + (2 * spec.id)]
    }

    companion object {
        fun checkPerfEventSupport(): String? = CpuCounterJni.checkPerfEventSupport()

        /**
         * Forces system properties and selinux into correct mode for capture
         *
         * Reset still required if failure occurs partway through
         */
        fun forceEnable(): String? {
            if (Build.VERSION.SDK_INT >= 29) {
                Api29Enabler.forceEnable()?.let { return it }
            }
            return checkPerfEventSupport()
        }
        fun reset() {
            if (Build.VERSION.SDK_INT >= 29) {
                Api29Enabler.reset()
            }
        }

        /**
         * Enable setenforce 0 and setprop perf_harden to 0, only observed this required on API 29+
         */
        @RequiresApi(29)
        object Api29Enabler {
            private val perfHardenProp = PropOverride("security.perf_harden", "0")
            private var shouldResetEnforce1 = false
            fun forceEnable(): String? {
                if (Shell.isSELinuxEnforced()) {
                    if (DeviceInfo.isRooted) {
                        Shell.executeScriptSilent("setenforce 0")
                        shouldResetEnforce1 = true
                    } else {
                        return "blocked by selinux, can't `setenforce 0` without rooted device"
                    }
                }
                perfHardenProp.forceValue()
                return null
            }

            fun reset() {
                perfHardenProp.resetIfOverridden()
                if (shouldResetEnforce1) {
                    Shell.executeScriptSilent("setenforce 1")
                    shouldResetEnforce1 = false
                }
            }
        }
    }
}

private object CpuCounterJni {
    init {
        System.loadLibrary("benchmarkNative")
    }

    // Profiler methods
    external fun checkPerfEventSupport(): String?
    external fun newProfiler(): Long
    external fun freeProfiler(profilerPtr: Long)
    external fun resetEvents(profilerPtr: Long, mask: Int): Int
    external fun reset(profilerPtr: Long)
    external fun start(profilerPtr: Long)
    external fun stop(profilerPtr: Long)
    external fun read(profilerPtr: Long, outData: LongArray)
}

internal fun List<CpuEventCounter.Event>.getFlags() = fold(0) { acc, event ->
    acc.or(event.flag)
}