Milan Jurkulak 3 năm trước cách đây
mục cha
commit
2b9b933bc8

+ 217 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/anr/agent/ANRSpyAgent.kt

@@ -0,0 +1,217 @@
+@file:Suppress("PrivatePropertyName", "LocalVariableName")
+
+package org.mjdev.tvlib.helpers.anr.agent
+
+import android.content.Context
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.os.MessageQueue.IdleHandler
+import org.mjdev.tvlib.helpers.anr.annotations.TraceClass
+import org.mjdev.tvlib.helpers.anr.annotations.TraceMethod
+import org.mjdev.tvlib.helpers.anr.models.MethodModel
+import timber.log.Timber
+
+class ANRSpyAgent constructor(
+    builder: Builder
+) : Thread() {
+
+    private var mContext: Context
+    private var mListener: ANRSpyListener? = null
+    private var mShouldThrowException: Boolean = false
+    private var TIME_OUT = 5000L
+    private var mReportAnnotatedMethods: Boolean = false
+    private var mListAnnotatedMedhods = mutableListOf<String>()
+    private var mReportMethods = mutableListOf<MethodModel>()
+    private val INTERVAL = 500L
+    private val mHandler = Handler(Looper.getMainLooper())
+    private var _timeWaited = 0L
+    private val _mTesterWorker = Runnable {
+        _timeWaited = 0L
+    }
+    private val mIdleHandler = IdleHandler {
+        mListener?.onReportAvailable(mReportMethods)
+        mReportMethods = mutableListOf()
+        true
+    }
+
+    init {
+        this.mListener = builder.getSpyListener()
+        this.mShouldThrowException = builder.getThrowException()
+        this.TIME_OUT = builder.getTimeOout()
+        this.mReportAnnotatedMethods = builder.getReportAnnotatedMethods()
+        this.mContext = builder.mContext
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            Looper.getMainLooper().queue.addIdleHandler(mIdleHandler)
+        } else {
+            Timber.e("Can not load queue handler.")
+            // todo iddle handler
+        }
+    }
+
+    class Builder(var mContext: Context) {
+
+        private var mListener: ANRSpyListener? = null
+        private var mShouldThrowException: Boolean = false
+        private var TIME_OUT = 5000L
+        private var mEnableInstantReport: Boolean = false
+
+        fun setSpyListener(listener: ANRSpyListener) = apply { this.mListener = listener }
+
+        fun getSpyListener() = this.mListener
+
+        fun setThrowException(throwexception: Boolean) = apply {
+            this.mShouldThrowException = throwexception
+        }
+
+        fun getThrowException() = this.mShouldThrowException
+
+        fun setTimeOut(timeout: Long) = apply { TIME_OUT = timeout }
+
+        fun getTimeOout() = TIME_OUT
+
+        fun enableReportAnnotatedMethods(enable: Boolean) = apply { mEnableInstantReport = enable }
+
+        fun getReportAnnotatedMethods() = this.mEnableInstantReport
+
+        fun build(): ANRSpyAgent = ANRSpyAgent(this)
+
+    }
+
+    @Suppress("UNUSED_VARIABLE")
+    override fun run() {
+        while (!isInterrupted) {
+            val stacktrace = Looper.getMainLooper().thread.stackTrace
+            isResumeExists(stacktrace)
+            _timeWaited += INTERVAL
+            mListener?.onWait(_timeWaited)
+            mHandler.post(_mTesterWorker)
+            sleep(INTERVAL)
+            if (_timeWaited > TIME_OUT) {
+                val listPackageMethods = findPackagMethods(stacktrace)
+                mListener?.onAnrDetected(
+                    "$THREAD_TITLE Main thread blocked for: $_timeWaited ms",
+                    stackTrace,
+                    findPackagMethods(stacktrace)
+                )
+                if (mShouldThrowException) {
+                    throwException(Looper.getMainLooper().thread.stackTrace)
+                }
+            }
+            if (mReportAnnotatedMethods) {
+                instantReport()
+            }
+        }
+    }
+
+    private fun isResumeExists(stacktrace: Array<StackTraceElement>) {
+        val allstacks = getAllStackTraces()
+        Timber.w("Threads------------")
+        for (entity in allstacks) {
+            Timber.e("${entity.key}")
+        }
+        Timber.w("Threads------------\nn")
+        Timber.w("Threads------------\n")
+        for (item in stacktrace) {
+            Timber.e(item.toString())
+        }
+        Timber.w("Threads------------" + "\n")
+    }
+
+    private fun throwException(stackTrace: Array<StackTraceElement>) {
+        throw ANRSpyException(THREAD_TITLE, stackTrace)
+    }
+
+    private fun instantReport() {
+        val stacktrace = Looper.getMainLooper().thread.stackTrace
+        for (element in stacktrace) {
+            addAnnotatedMethods(element.className)
+            val methodexists = mListAnnotatedMedhods.find {
+                element.methodName.lowercase().startsWith(it.lowercase())
+            }
+            if (methodexists != null) {
+                addMethod(methodexists, Looper.getMainLooper().thread)
+                return
+            }
+        }
+    }
+
+    @Synchronized
+    fun addMethod(methodName: String, thread: Thread) {
+        val exists = mReportMethods.find {
+            (it.name.lowercase() == methodName.lowercase())
+        }
+        if (exists != null) {
+            for (item in mReportMethods) {
+                if (item.name.lowercase() == exists.name.lowercase()) {
+                    item.elapsedTime += INTERVAL
+                    return
+                }
+            }
+        } else {
+            mReportMethods.add(
+                MethodModel(
+                    System.currentTimeMillis(), methodName, thread, 0
+                )
+            )
+        }
+    }
+
+    private fun addAnnotatedMethods(className: String) {
+        try {
+            val clazz = Class.forName(className)
+            val annotation = clazz.getAnnotation(TraceClass::class.java)
+            annotation?.let { tc ->
+                if (tc.traceAllMethods) {
+                    for (m in clazz.declaredMethods) {
+                        val exists = mListAnnotatedMedhods.find { s ->
+                            s.lowercase() == m.name.lowercase()
+                        }
+                        if (exists == null) {
+                            mListAnnotatedMedhods.add(m.name)
+                        }
+                    }
+                } else {
+                    for (m in clazz.declaredMethods) {
+                        m.getAnnotation(TraceMethod::class.java).let { ann ->
+                            ann?.let {
+                                val exists = mListAnnotatedMedhods.find {
+                                    it.lowercase() == m.name.lowercase()
+                                }
+                                if (exists == null) {
+                                    mListAnnotatedMedhods.add(m.name)
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (ex: Exception) {
+            mListener?.onError(ex.message.toString())
+        }
+    }
+
+    private fun findPackagMethods(stacktrace: Array<StackTraceElement>): List<String>? {
+        val list = mutableListOf<String>()
+        val seq_strack = stacktrace.toList()
+        val filtered = seq_strack.filter {
+            it.className.lowercase().contains(mContext.packageName.lowercase())
+        }
+        if (filtered.isNotEmpty()) {
+            filtered.forEach { st ->
+                val body =
+                    "Class: ${st.className} Method: ${st.methodName} LineNumber<${st.lineNumber}>(${st.fileName})"
+                list.add(body)
+            }
+            return list
+        }
+        return null
+    }
+
+    companion object {
+
+        const val THREAD_TITLE = "[ ++ ANR Spy ++ ]"
+
+    }
+
+}

+ 9 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/anr/agent/ANRSpyException.kt

@@ -0,0 +1,9 @@
+package org.mjdev.tvlib.helpers.anr.agent
+
+class ANRSpyException(title:String, stacktrace:Array<StackTraceElement>):Throwable(title) {
+
+    init {
+        stackTrace = stacktrace
+    }
+
+}

+ 24 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/anr/agent/ANRSpyListener.kt

@@ -0,0 +1,24 @@
+package org.mjdev.tvlib.helpers.anr.agent
+
+import org.mjdev.tvlib.helpers.anr.models.MethodModel
+
+interface ANRSpyListener {
+
+    fun onWait(ms: Long) {}
+
+    @Deprecated("This callback will be removed in future")
+    fun onAnrStackTrace(stackstrace: Array<StackTraceElement>) {
+    }
+
+    fun onAnrDetected(
+        details: String,
+        stackTrace: Array<StackTraceElement>,
+        packageMethods: List<String>?
+    ) {
+    }
+
+    fun onReportAvailable(methodList: List<MethodModel>) {}
+
+    fun onError(error: String) {}
+
+}

+ 52 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/anr/agent/SpyLooper.kt

@@ -0,0 +1,52 @@
+package org.mjdev.tvlib.helpers.anr.agent
+
+import android.annotation.SuppressLint
+import android.os.Looper
+import android.os.Message
+import android.os.MessageQueue
+import timber.log.Timber
+import java.lang.reflect.Field
+
+@SuppressLint("PrivateApi")
+class SpyLooper {
+
+    var messagesField: Field
+    var nextField: Field
+    var mainMessageQueue: MessageQueue
+
+    init {
+        try {
+            val queueField = Looper::class.java.getDeclaredField("mQueue")
+            queueField.isAccessible = true
+            messagesField = MessageQueue::class.java.getDeclaredField("mMessages")
+            messagesField.isAccessible = true
+            nextField = Message::class.java.getDeclaredField("next")
+            nextField.isAccessible = true
+            val mainLooper = Looper.getMainLooper()
+            mainMessageQueue = queueField[mainLooper] as MessageQueue
+        } catch (e: Exception) {
+            throw RuntimeException(e)
+        }
+    }
+
+    fun dumpQueue() {
+        try {
+            val nextMessage = messagesField[mainMessageQueue] as Message
+            Timber.d("Begin dumping queue")
+            dumpMessages(nextMessage)
+            Timber.d("End dumping queue")
+        } catch (e: IllegalAccessException) {
+            throw RuntimeException(e)
+        }
+    }
+
+    @Throws(IllegalAccessException::class)
+    fun dumpMessages(message: Message?) {
+        message?.let {
+            Timber.w("Message: $message")
+            val next = nextField[message] as Message?
+            dumpMessages(next)
+        }
+    }
+
+}

+ 7 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/anr/annotations/TraceClass.kt

@@ -0,0 +1,7 @@
+package org.mjdev.tvlib.helpers.anr.annotations
+
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class TraceClass(
+    val traceAllMethods:Boolean = true
+)

+ 5 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/anr/annotations/TraceMethod.kt

@@ -0,0 +1,5 @@
+package org.mjdev.tvlib.helpers.anr.annotations
+
+@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class TraceMethod()

+ 11 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/anr/models/MethodModel.kt

@@ -0,0 +1,11 @@
+package org.mjdev.tvlib.helpers.anr.models
+
+import androidx.annotation.Keep
+
+@Keep
+data class MethodModel(
+    val id: Long,
+    val name: String,
+    val thread: Thread,
+    var elapsedTime: Long
+)