ThreadPriority.kt

/*
 * Copyright 2019 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 android.os.Process
import android.util.Log
import androidx.annotation.GuardedBy
import androidx.benchmark.BenchmarkState.Companion.TAG
import java.io.File
import java.io.IOException

internal object ThreadPriority {
    /**
     * Max priority for a linux process, see docs of android.os.Process
     *
     * For some reason, constant not provided as platform API.
     */
    const val HIGH_PRIORITY = -20

    private const val BENCH_THREAD_PRIORITY = HIGH_PRIORITY
    /**
     * Set JIT lower than bench thread, to reduce chance of it preempting during measurement
     */
    private const val JIT_THREAD_PRIORITY =
        HIGH_PRIORITY + Process.THREAD_PRIORITY_LESS_FAVORABLE * 5

    private const val TASK_PATH = "/proc/self/task"
    private const val JIT_THREAD_NAME = "Jit thread pool"
    private val JIT_TID: Int?
    val JIT_INITIAL_PRIORITY: Int

    init {
        if (Build.VERSION.SDK_INT >= 24) {
            // JIT thread expected to exist on N+ devices
            val tidsToNames = File(TASK_PATH).listFiles()?.associateBy(
                {
                    // tid
                    it.name.toInt()
                },
                {
                    // thread name
                    try {
                        File(it, "comm").readLines().firstOrNull() ?: ""
                    } catch (e: IOException) {
                        // if we fail to read thread name, file may not exist because thread
                        // died. Expect no error reading Jit thread name, so just name thread
                        // incorrectly.
                        "ERROR READING THREAD NAME"
                    }
                }
            )
            if (tidsToNames.isNullOrEmpty()) {
                Log.d(TAG, "NOTE: Couldn't find threads in this process for priority pinning.")
                JIT_TID = null
            } else {
                JIT_TID =
                    tidsToNames.filter { it.value.startsWith(JIT_THREAD_NAME) }.keys.firstOrNull()
                if (JIT_TID == null) {
                    Log.d(TAG, "NOTE: Couldn't JIT thread, threads found:")
                    tidsToNames.forEach {
                        Log.d(TAG, "    tid: ${it.key}, name:'${it.value}'")
                    }
                }
            }
        } else {
            JIT_TID = null
        }

        JIT_INITIAL_PRIORITY = if (JIT_TID != null) Process.getThreadPriority(JIT_TID) else 0
    }

    private val lock = Any()

    @GuardedBy("lock")
    private var initialTid: Int = -1
    @GuardedBy("lock")
    private var initialPriority: Int = Int.MAX_VALUE

    /*
     * [android.os.Process.getThreadPriority] is not very clear in which conditions it will fail,
     * so setting JIT / benchmark thread priorities are best-effort for now
     */
    private fun setThreadPriority(label: String, tid: Int, priority: Int): Boolean {
        val previousPriority = Process.getThreadPriority(tid)
        try {
            Process.setThreadPriority(tid, priority)
        } catch (e: SecurityException) {
            return false
        }

        val newPriority = Process.getThreadPriority(tid)
        if (newPriority != previousPriority) {
            Log.d(
                TAG,
                "Set $tid ($label) to priority $priority. Was $previousPriority, now $newPriority"
            )
            return true
        }
        return false
    }

    /**
     * Bump thread priority of the current thread and JIT to be high, resetting any other bumped
     * thread.
     *
     * Only one benchmark thread can be be bumped at a time.
     */
    fun bumpCurrentThreadPriority() = synchronized(lock) {
        val myTid = Process.myTid()
        if (initialTid == myTid) {
            // already bumped
            return
        }

        // ensure we don't have multiple threads bumped at once
        resetBumpedThread()

        initialTid = myTid
        initialPriority = Process.getThreadPriority(initialTid)

        setThreadPriority("Bench thread", initialTid, BENCH_THREAD_PRIORITY)
        if (JIT_TID != null) {
            setThreadPriority("Jit", JIT_TID, JIT_THREAD_PRIORITY)
        }
    }

    fun resetBumpedThread() = synchronized(lock) {
        if (initialTid > 0) {
            setThreadPriority("Bench thread", initialTid, initialPriority)
            if (JIT_TID != null) {
                setThreadPriority("Jit", JIT_TID, JIT_INITIAL_PRIORITY)
            }
            initialTid = -1
        }
    }

    fun getJit(): Int {
        checkNotNull(JIT_TID) { "Jit thread not found!" }
        return Process.getThreadPriority(JIT_TID)
    }

    fun get(): Int {
        return Process.getThreadPriority(Process.myTid())
    }
}