1 /*
<lambda>null2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.benchmark
18 
19 import android.os.Build
20 import androidx.annotation.RequiresApi
21 import androidx.annotation.RestrictTo
22 import java.io.Closeable
23 import java.util.Locale
24 
25 /**
26  * Exposes CPU counters from perf_event_open based on libs/utils/src/Profiler.cpp from
27  * Google/Filament.
28  *
29  * This layer is extremely simple to reduce overhead, though it does not yet use fast/critical JNI.
30  *
31  * This counter must be closed to avoid leaking the associated native allocation.
32  *
33  * This class does not yet help callers with prerequisites to getting counter values on API 23+:
34  * - setenforce 0 (requires root)
35  * - security.perf_harden 0
36  */
37 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
38 class CpuEventCounter : Closeable {
39     private var profilerPtr = CpuCounterJni.newProfiler()
40     private var hasReset = false
41     internal var currentEventFlags = 0
42         private set
43 
44     /** updated in sync with currentEventFlags, tracks those that should never be zero */
45     private var validateFlags = 0
46 
47     fun resetEvents(events: List<Event>) {
48         resetEvents(events.getFlags())
49     }
50 
51     fun resetEvents(eventFlags: Int) {
52         if (currentEventFlags != eventFlags) {
53             // set up the flags
54             CpuCounterJni.resetEvents(profilerPtr, eventFlags)
55             currentEventFlags = eventFlags
56             validateFlags = currentEventFlags.and(Event.CpuCycles.flag.or(Event.Instructions.flag))
57         } else {
58             // fast path when re-using same flags
59             reset()
60         }
61         hasReset = true
62     }
63 
64     override fun close() {
65         CpuCounterJni.freeProfiler(profilerPtr)
66         profilerPtr = 0
67     }
68 
69     fun reset() {
70         CpuCounterJni.reset(profilerPtr)
71     }
72 
73     fun start() = CpuCounterJni.start(profilerPtr)
74 
75     fun stop() = CpuCounterJni.stop(profilerPtr)
76 
77     fun read(outValues: Values) {
78         check(profilerPtr != 0L) { "Error: attempted to read counters after close" }
79         check(hasReset) { "Error: attempted to read counters without reset" }
80         CpuCounterJni.read(profilerPtr, outValues.longArray)
81         if (validateFlags != 0) {
82             val hasInstructionError =
83                 validateFlags.and(Event.Instructions.flag) != 0 &&
84                     outValues.getValue(Event.Instructions) == 0L
85             val hasCpuCyclesError =
86                 validateFlags.and(Event.CpuCycles.flag) != 0 &&
87                     outValues.getValue(Event.CpuCycles) == 0L
88             check(!hasInstructionError && !hasCpuCyclesError) {
89                 val events = Event.entries.filter { it.flag.and(currentEventFlags) != 0 }
90                 "Observed 0 for instructions/cpuCycles, capture appeared to fail, values=[" +
91                     events.joinToString(",") { it.outputName + "=" + outValues.getValue(it) } +
92                     "]"
93             }
94         }
95     }
96 
97     enum class Event(val id: Int) {
98         Instructions(0),
99         CpuCycles(1),
100         L1DReferences(2),
101         L1DMisses(3),
102         BranchInstructions(4),
103         BranchMisses(5),
104         L1IReferences(6),
105         L1IMisses(7);
106 
107         val flag: Int
108             inline get() = 1 shl id
109 
110         val outputName = name.replaceFirstChar { it.lowercase(Locale.US) }
111     }
112 
113     /**
114      * Holder class for querying all counter values at once out of native, to avoid multiple JNI
115      * transitions.
116      */
117     @JvmInline
118     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
119     value class Values(val longArray: LongArray = LongArray(19)) {
120         init {
121             // See CountersLongCount static_assert in native
122             require(longArray.size == 19)
123         }
124 
125         inline val numberOfCounters: Long
126             get() = longArray[0]
127 
128         inline val timeEnabled: Long
129             get() = longArray[1]
130 
131         inline val timeRunning: Long
132             get() = longArray[2]
133 
134         @Suppress("NOTHING_TO_INLINE")
135         inline fun getValue(spec: Event): Long = longArray[3 + (2 * spec.id)]
136     }
137 
138     companion object {
139         const val MIN_API_ROOT_REQUIRED = 23
140 
141         fun checkPerfEventSupport(): String? = CpuCounterJni.checkPerfEventSupport()
142 
143         /**
144          * Forces system properties and selinux into correct mode for capture
145          *
146          * Reset still required if failure occurs partway through
147          */
148         fun forceEnable(): String? {
149             if (Build.VERSION.SDK_INT >= 23) {
150                 Api23Enabler.forceEnable()?.let {
151                     return it
152                 }
153             }
154             return checkPerfEventSupport()
155         }
156 
157         fun reset() {
158             if (Build.VERSION.SDK_INT >= 23) {
159                 Api23Enabler.reset()
160             }
161         }
162 
163         /**
164          * Enable setenforce 0 and setprop perf_harden to 0, have observed this required on API 23+
165          *
166          * Lower APIs not tested, but selinux is documented to be enforced starting in Android 5
167          * (API 23).
168          */
169         @RequiresApi(23)
170         object Api23Enabler {
171             private val perfHardenProp = PropOverride("security.perf_harden", "0")
172             private var shouldResetEnforce1 = false
173 
174             fun forceEnable(): String? {
175                 if (Shell.isSELinuxEnforced()) {
176                     if (DeviceInfo.isRooted) {
177                         Shell.executeScriptSilent("setenforce 0")
178                         shouldResetEnforce1 = true
179                     } else {
180                         return "blocked by selinux, can't `setenforce 0` without rooted device"
181                     }
182                 }
183                 perfHardenProp.forceValue()
184                 return null
185             }
186 
187             fun reset() {
188                 perfHardenProp.resetIfOverridden()
189                 if (shouldResetEnforce1) {
190                     Shell.executeScriptSilent("setenforce 1")
191                     shouldResetEnforce1 = false
192                 }
193             }
194         }
195     }
196 }
197 
198 private object CpuCounterJni {
199     init {
200         System.loadLibrary("benchmarkNative")
201     }
202 
203     // Profiler methods
checkPerfEventSupportnull204     external fun checkPerfEventSupport(): String?
205 
206     external fun newProfiler(): Long
207 
208     external fun freeProfiler(profilerPtr: Long)
209 
210     external fun resetEvents(profilerPtr: Long, mask: Int): Int
211 
212     external fun reset(profilerPtr: Long)
213 
214     external fun start(profilerPtr: Long)
215 
216     external fun stop(profilerPtr: Long)
217 
218     external fun read(profilerPtr: Long, outData: LongArray)
219 }
220 
221 internal fun List<CpuEventCounter.Event>.getFlags() = fold(0) { acc, event -> acc.or(event.flag) }
222